Back to Articles

Watchtower: When HTML5 LocalStorage Became a Security Audit Database

[ View on GitHub ]

Watchtower: When HTML5 LocalStorage Became a Security Audit Database

Hook

Before modern SAST platforms required cloud backends and authentication layers, one Ruby tool turned your browser into a persistent audit workstation using nothing but HTML5 LocalStorage—no database, no server, no login.

Context

In the late 2000s and early 2010s, security auditors faced a workflow problem that expensive commercial tools hadn't solved: maintaining state during multi-day manual code reviews. Automated static analysis tools would generate thousands of findings, but tracking which issues you'd already examined, which were false positives, and which required deeper investigation meant either expensive proprietary platforms or elaborate spreadsheet gymnastics. Chris Lane built Watchtower to solve this friction by recognizing that the audit process itself—not just the scanning—needed tooling.

The insight was architectural: if the browser could remember your audit decisions using HTML5 LocalStorage, you didn't need a database server, authentication system, or deployment infrastructure. Generate a static HTML report once, open it locally, and your review annotations would persist across sessions automatically. For consultancies performing dozens of code audits annually, this meant zero server costs and instant deployment—just run the scanner, hand the HTML file to your auditor, and their work state would survive browser restarts without any backend. Watchtower carved out a niche for manual security reviews where heavyweight commercial platforms were overkill and grep-based workflows were too primitive.

Technical Insight

Output Layer

Signature System

scan command

recursive walk

load by extension

file content

matches + metadata

format selection

CSV

XML

TXT

HTML

CLI Entry Point

Directory Scanner

Source Files

Signature Configs

Pattern Matching Engine

Match Results

Reporter Factory

CSV Export

XML Export

Text Report

HTML + LocalStorage UI

System architecture — auto-generated

Watchtower's architecture centers on a signature-based pattern matching engine with a clever reporting layer. The core scanning logic recursively walks directory trees, applies configurable signatures organized by file extension, and collects matches. What makes it interesting isn't the scanning itself—it's fundamentally regex-over-files—but how signatures are structured and how results become interactive.

Signatures live in Ruby configuration files organized by category and file type. Here's how you'd define patterns to catch SQL injection vulnerabilities in PHP:

# signatures/php/sql-injection.rb
{
  :name        => 'SQL Injection Vectors',
  :category    => 'security',
  :file_types  => ['php'],
  :signatures  => [
    {
      :name    => 'Direct query execution',
      :match   => /mysql_query\s*\(\s*\$[a-zA-Z_]+/,
      :caption => 'User input may reach mysql_query without sanitization'
    },
    {
      :name    => 'String concatenation in SQL',
      :match   => /query\s*=\s*["'].*\$[a-zA-Z_]+/,
      :caption => 'Variable interpolation in SQL string'
    }
  ]
}

This signature structure separates concerns cleanly: the :file_types array determines when to apply these patterns, :match can be either literal strings or regexes, and :caption provides context for auditors. You can extend Watchtower for new frameworks by dropping signature files into the appropriate directory—WordPress-specific vulnerabilities go in signatures/php/wordpress.rb, Django patterns in signatures/python/django.rb.

The real architectural innovation emerges in the HTML reporter. When Watchtower generates an interactive report, it creates a self-contained HTML file with embedded JavaScript that uses LocalStorage as a persistence layer:

// Simplified excerpt from the HTML reporter
function markFinding(findingId, status) {
  var findings = JSON.parse(localStorage.getItem('audit_state') || '{}');
  findings[findingId] = {
    status: status,  // 'good', 'bad', or 'unsure'
    timestamp: new Date().toISOString(),
    reviewer: localStorage.getItem('reviewer_name')
  };
  localStorage.setItem('audit_state', JSON.stringify(findings));
  updateDisplay(findingId, status);
}

function loadAuditState() {
  var findings = JSON.parse(localStorage.getItem('audit_state') || '{}');
  for (var id in findings) {
    updateDisplay(id, findings[id].status);
  }
}

window.addEventListener('DOMContentLoaded', loadAuditState);

When an auditor clicks "Mark as False Positive" or "Confirm Vulnerability," the browser serializes that decision to LocalStorage keyed by finding ID. On subsequent page loads, loadAuditState() rehydrates the interface, visually indicating which findings you've already triaged. This creates a stateful workflow without TCP connections—your entire audit database lives in ~/.config/google-chrome/Default/Local Storage or equivalent.

The multi-format output system demonstrates thoughtful integration design. CSV and XML exports allow importing results into ticketing systems or spreadsheets, while the TXT format provides grep-friendly output for command-line workflows. Each reporter implements a common interface, making it trivial to add new formats:

class Reporter::Custom
  def initialize(findings, options)
    @findings = findings
    @options  = options
  end
  
  def generate
    # Transform findings array into your desired format
    @findings.map { |f| format_finding(f) }.join("\n")
  end
end

This plugin architecture meant teams could create custom reporters for their specific toolchains—imagine generating GitHub Issues markdown or JIRA XML directly from scan results.

The signature organization by semantic groups (authentication, session-management, cryptography) rather than just file types shows mature domain modeling. An auditor can scan only for cryptographic weaknesses across all languages, or focus on PHP session handling specifically. This granularity transforms a simple grep replacement into a guided review workflow.

Gotcha

Watchtower is officially deprecated—the author explicitly directs users to 'drek' as its successor. This isn't a soft deprecation; it's a hard recommendation to migrate. Any investment in learning Watchtower's signature syntax or building custom reporters is wasted effort when the author has abandoned it.

Beyond deprecation, the Ruby 1.8 compatibility issue creates a subtle but annoying problem: hash iteration order is non-deterministic in Ruby 1.8, meaning findings appear in random order across report generations. If you're comparing today's scan against last week's, identical findings may appear at different positions, breaking diff-based workflows. This is fixed in Ruby 1.9+, but speaks to the tool's age.

The LocalStorage-based persistence, while clever, has hard limits. Browser storage is typically capped at 5-10MB, meaning massive codebases with thousands of findings could exceed quota. More problematically, LocalStorage is origin-bound—if you open the HTML report from file:///home/user/audit1.html versus file:///tmp/audit1.html, you get different storage contexts, fragmenting your audit state. Sharing audit progress across a team requires manually exporting/importing the LocalStorage JSON, which defeats the simplicity advantage. For solo auditors this works beautifully; for team collaboration it's friction-prone.

Remote scanning via wget mirroring creates duplicate noise when pages are accessible through multiple URLs (example.com/page vs example.com/page/ vs example.com/page/index.php), and you're only seeing rendered output, not server-side source—missing the actual vulnerability surface in most web applications.

Verdict

Skip if: You're starting a new security audit workflow, building production tooling, or need team collaboration features. Watchtower is deprecated, and 'drek' is the author's recommended successor for this use case. Modern alternatives like Semgrep offer superior accuracy, community rule libraries, and CI/CD integration. Use if: You're maintaining legacy systems already using Watchtower, studying the design pattern of browser-based stateful reporting without backend infrastructure, or need a quick educational example of signature-based scanning architecture. The codebase remains valuable as a reference implementation for LocalStorage-powered workflows and plugin-based reporter systems, but invest your time in drek or contemporary SAST platforms for actual security work.

// ADD TO YOUR README
[![Featured on Starlog](https://starlog.is/api/badge/cybersecurity/chrisallenlane-watchtower.svg)](https://starlog.is/api/badge-click/cybersecurity/chrisallenlane-watchtower)