diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..5cd280c --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,8 @@ +## Codebase Overview + +Gecko is a cross-browser (Chrome/Firefox) Manifest V3 extension that automates discovery of Client-Side Path Traversals (CSPT) by intercepting HTTP requests and matching URL-derived values against request paths. Findings are surfaced in a DevTools panel with search, filtering, and export capabilities. + +**Stack**: TypeScript, React 18, Tailwind CSS 3, Headless UI, Webpack 5, webextension-polyfill +**Structure**: Service worker (`src/sw/`) handles detection, React UI split between DevTools panel (`src/pages/panel.tsx`) and popup settings (`src/pages/popup.tsx`), shared types/constants in `src/shared/`, reusable UI components in `src/components/ui/`. `browser.storage.local` is the single shared state bus between all contexts. + +For detailed architecture, see [docs/CODEBASE_MAP.md](docs/CODEBASE_MAP.md). diff --git a/docs/CODEBASE_MAP.md b/docs/CODEBASE_MAP.md new file mode 100644 index 0000000..7cb2248 --- /dev/null +++ b/docs/CODEBASE_MAP.md @@ -0,0 +1,401 @@ +--- +last_mapped: 2026-04-13T20:09:39Z +total_files: 35 +total_tokens: 13029 +--- + +# Codebase Map + +> Auto-generated by Cartographer. Last mapped: 2026-04-13T20:09:39Z + +## System Overview + +Gecko is a cross-browser (Chrome/Firefox) Manifest V3 extension that automates discovery of **Client-Side Path Traversals (CSPT)**. It intercepts outbound HTTP requests via a service worker, extracts "sources" (query parameter values, path segments, null/undefined literals) from the active tab's URL, and checks whether any source value appears in the path of the intercepted request. Matches are called **findings** and are surfaced in a DevTools panel. + +```mermaid +graph TB + subgraph Browser + Tab[Active Tab URL] + Requests[Outbound HTTP Requests] + end + subgraph Extension + SW[Service Worker
sw.ts] + Storage[(browser.storage.local)] + Panel[DevTools Panel
panel.tsx] + Popup[Popup Settings
popup.tsx] + DevTools[DevTools Page
devtools.js] + end + Tab -->|"urlToSources()"| SW + Requests -->|"onBeforeRequest"| SW + SW -->|"storeFinding()"| Storage + SW -->|"badge count"| Browser + Storage -->|"onChanged listener"| Panel + Popup -->|"write settings"| Storage + Storage -->|"onChanged listener"| SW + DevTools -->|"panels.create()"| Panel +``` + +## Directory Structure + +``` +gecko/ +├── public/ # Static assets copied to dist/ +│ ├── icons/ # Extension icons (48px, 96px) +│ ├── js/ +│ │ └── devtools.js # Hand-authored DevTools panel registration (not bundled) +│ ├── devtools.html # DevTools page entry point +│ ├── manifest-chrome.json # Chrome MV3 manifest +│ ├── manifest-ff.json # Firefox MV3 manifest +│ ├── panel.html # DevTools panel React shell +│ └── popup.html # Browser action popup React shell +├── src/ +│ ├── components/ +│ │ ├── ui/ # Stateless presentation components +│ │ │ ├── empty-findings.tsx # Empty state placeholder +│ │ │ ├── highlithed-text.tsx# Text highlighting with URL awareness (filename typo) +│ │ │ ├── info-alert.tsx # Blue informational alert box +│ │ │ ├── search-input.tsx # Reusable search input with icon +│ │ │ └── toggle.tsx # Accessible on/off switch (Headless UI) +│ │ ├── finding-data-display.tsx # Detail view for a single finding +│ │ ├── finding-drawer.tsx # Slide-over drawer (Headless UI Dialog) +│ │ ├── findings-context.tsx # React Context: state, filtering, storage sync +│ │ ├── findings-search.tsx # Simple + advanced search bar +│ │ └── findings-table.tsx # Main findings list with export/clear +│ ├── pages/ +│ │ ├── panel.tsx # DevTools panel entry point (Webpack: panel.js) +│ │ └── popup.tsx # Popup settings entry point (Webpack: popup.js) +│ ├── shared/ +│ │ ├── constants.ts # Default settings object +│ │ └── types.ts # All TypeScript interfaces and enums +│ ├── sw/ +│ │ └── sw.ts # Service worker: CSPT detection engine +│ └── tailwind/ +│ └── styles.css # Tailwind CSS entry (@tailwind directives) +├── tests/ +│ ├── index.html # Manual test: query-value-to-path CSPT +│ └── test/ +│ └── path.html # Manual test: path-to-path CSPT +├── Dockerfile # Node 20 Alpine build image +├── build-docker.sh # Docker build helper (chrome|ff) +├── package.json # Scripts, dependencies, metadata +├── postcss.config.js # Tailwind + autoprefixer +├── tailwind.config.js # Content paths, custom danger color +├── tsconfig.json # TypeScript config (IDE only, not used by webpack) +├── webpack.config.js # 3 entry points, babel-loader, copy plugin +├── README.md # Project documentation +├── PRIVACY.md # Privacy policy for Chrome Web Store +└── todo.md # Development task tracker +``` + +## Module Guide + +### Service Worker (`src/sw/sw.ts`) + +**Purpose**: Core CSPT detection engine — intercepts all outbound requests and matches them against values extracted from the active tab URL. + +**Key files**: +| File | Purpose | Tokens | +|------|---------|--------| +| `src/sw/sw.ts` | Request interception, source extraction, finding generation, storage | ~1,530 | + +**Key functions**: +- `urlToSources(url)` — Parses the active tab URL; emits `Source[]` for each query param value (raw, encoded, decoded), each path segment (raw, encoded, decoded), and optionally `"undefined"`/`"null"` literals. +- `generateFindings(url, sources)` — Splits the target request URL path by `/`; checks each segment against each source value (exact or partial match, case-insensitive per settings). Hard-ignores `adservice.google.com` and `ad.doubleclick.net`. +- `storeFinding(finding)` — Mutex-protected read-modify-write to `browser.storage.local`. Deduplicates via `findingsCache` keyed by `${source.url}-${source.value}-${target.url}`. +- `updateCurrentTab()` — Queries active tab and caches it module-level. + +**Event listeners**: `webRequest.onBeforeRequest`, `tabs.onActivated`, `tabs.onUpdated`, `webNavigation.onCommitted`, `storage.local.onChanged`. + +**Dependencies**: `webextension-polyfill`, `async-mutex`, `shared/types`, `shared/constants`. + +**Gotchas**: +- `storeFinding` is called per-finding in a loop with no batching — each acquires the mutex separately. Acknowledged by a `// TODO` comment. +- Cache key does not include `SourceType`, so two sources with the same URL and value but different types are treated as duplicates. +- `currentSettings` is mutated via `Object.assign` on storage changes — races with in-flight `onBeforeRequest` callbacks. +- `partialMinLength` check is per-source-value, not global. + +--- + +### Shared Types & Constants (`src/shared/`) + +**Purpose**: Central type definitions and default configuration shared by all layers. + +| File | Purpose | Tokens | +|------|---------|--------| +| `src/shared/types.ts` | Enums (`SourceType`), interfaces (`Source`, `Target`, `Finding`, `FindingUI`, `Settings`, `Search`) | ~283 | +| `src/shared/constants.ts` | `defaultSettings` object with all scanner/matching/display/filter defaults | ~96 | + +**Default settings**: All scanners on, partial matching off, case-insensitive on, `partialMinLength: 3`, `clearOnRefresh: false`, empty ignore patterns. + +**Gotchas**: +- `SourceType` enum values are human-readable strings (e.g., `"Query Parameter (URL encoded)"`) used directly as UI labels. +- `sw.ts` deep-clones `defaultSettings` via `JSON.parse(JSON.stringify(...))` to avoid reference aliasing. + +--- + +### Findings Context (`src/components/findings-context.tsx`) + +**Purpose**: React Context provider that owns all findings state, filtering logic, and storage synchronization for the DevTools panel. + +| File | Purpose | Tokens | +|------|---------|--------| +| `src/components/findings-context.tsx` | Context provider, storage listeners, filter pipeline | ~1,185 | + +**Exports**: `FindingsProvider`, `useFindings()` hook. + +**Context value**: `{ findings, clearFindings, refreshFindings, search, setSearch }`. + +**Filter pipeline** (runs on every change to `search`, `rawFindings`, or `settings`): +1. **Ignore patterns**: Each pattern from `settings.filters.ignorePatterns` compiled as `RegExp`, tested against target URL pathname. Invalid patterns silently ignored. +2. **Search terms**: Space-split tokens with `-prefix` negation (AND logic across all terms per field). + +**Dependencies**: `webextension-polyfill`, `shared/types`, `shared/constants`. + +**Gotchas**: +- Two separate `storage.local.onChanged` listeners (settings + findings) in the same component. +- `clearFindings` sets local state to `[]` immediately AND writes to storage, causing a redundant re-read when the storage listener fires. +- Negative search syntax (`-term`) is not documented in the UI. + +--- + +### Findings Table & Search (`src/components/findings-table.tsx`, `findings-search.tsx`) + +**Purpose**: Main data table listing findings with search, export, and clear controls. + +| File | Purpose | Tokens | +|------|---------|--------| +| `src/components/findings-table.tsx` | Table rendering, export JSON, clear, row click handling | ~1,391 | +| `src/components/findings-search.tsx` | Simple/advanced search mode toggle with SearchInput fields | ~379 | + +**Export behavior**: Creates a `Blob` of `JSON.stringify(findings)`, triggers a synthetic anchor click. Exports the **unfiltered** findings from context, not the currently visible filtered set. + +**Dependencies**: `findings-context`, `findings-search`, `ui/highlithed-text`, `ui/empty-findings`, `@heroicons/react`. + +**Gotchas**: +- `.reverse()` mutates the findings array in place — a code smell though not currently causing bugs. +- Export downloads unfiltered findings, which may surprise users who applied filters. +- `FindingsTable` imports `tailwind/styles.css` — `panel.tsx` relies on this transitive import. + +--- + +### Finding Detail View (`src/components/finding-drawer.tsx`, `finding-data-display.tsx`) + +**Purpose**: Slide-over drawer showing full detail for a selected finding. + +| File | Purpose | Tokens | +|------|---------|--------| +| `src/components/finding-drawer.tsx` | Headless UI Dialog slide-over panel | ~640 | +| `src/components/finding-data-display.tsx` | Structured detail display (value, source, target, type) | ~614 | + +**Dependencies**: `@headlessui/react`, `@heroicons/react`, `ui/highlithed-text`, `shared/types`. + +**Gotchas**: +- `new URL(...)` calls in `finding-data-display.tsx` are unguarded — malformed URLs would throw. +- `"use client"` directive in `finding-drawer.tsx` is a Next.js artifact with no effect. +- Target URL in detail view shows full URL (with protocol), while the table shows protocol-stripped version. + +--- + +### UI Primitives (`src/components/ui/`) + +**Purpose**: Stateless, reusable presentation components. + +| File | Purpose | Tokens | +|------|---------|--------| +| `src/components/ui/toggle.tsx` | Accessible toggle switch (Headless UI Switch) | ~394 | +| `src/components/ui/search-input.tsx` | Search input with optional label and icon | ~396 | +| `src/components/ui/highlithed-text.tsx` | Text highlighting with URL-aware splitting | ~362 | +| `src/components/ui/info-alert.tsx` | Blue informational alert box | ~161 | +| `src/components/ui/empty-findings.tsx` | Empty state placeholder | ~102 | + +**Gotchas**: +- `highlithed-text.tsx` filename is misspelled (should be "highlighted") — consistent across all imports. +- `empty-findings.tsx` imports `PlusIcon` but never uses it (dead import). +- `search-input.tsx` passes optional `value` prop directly to `` — `undefined` causes controlled/uncontrolled switch warning. + +--- + +### Pages / Entry Points (`src/pages/`) + +**Purpose**: Webpack entry points that bootstrap independent React trees. + +| File | Purpose | Tokens | +|------|---------|--------| +| `src/pages/panel.tsx` | DevTools panel — FindingsTable + FindingDrawer inside FindingsProvider | ~291 | +| `src/pages/popup.tsx` | Browser action popup — settings toggles, ignore patterns textarea | ~1,317 | + +**Gotchas**: +- `popup.tsx` has a subtle race: `useEffect([settings])` writes `defaultSettings` to storage on initial render before the async storage read completes. +- `panel.tsx` does not clear `selectedFinding` on drawer close — intentional for smooth exit animation. +- Both files bootstrap via `createRoot(document.getElementById("root"))` at module evaluation time. + +--- + +### Browser Integration (`public/`) + +**Purpose**: Static assets, HTML shells, and manifest definitions copied to `dist/` by CopyWebpackPlugin. + +| File | Purpose | Tokens | +|------|---------|--------| +| `public/manifest-chrome.json` | Chrome MV3 manifest (ES module service worker) | ~148 | +| `public/manifest-ff.json` | Firefox MV3 manifest (`scripts` array fallback) | ~153 | +| `public/devtools.html` | Invisible DevTools page that loads devtools.js | ~27 | +| `public/js/devtools.js` | Registers the Gecko panel via `panels.create()` | ~44 | +| `public/panel.html` | React shell for DevTools panel | ~69 | +| `public/popup.html` | React shell for browser action popup | ~69 | + +**Chrome vs Firefox differences**: +| Aspect | Chrome | Firefox | +|--------|--------|---------| +| Background type | `"type": "module"` | Absent (classic script) | +| Background field | `service_worker` only | `service_worker` + `scripts` array | + +**Gotchas**: +- `devtools.js` uses manual `window.browser || window.chrome` polyfill — NOT `webextension-polyfill`. +- `public/manifest.json` is `.gitignore`d — generated at build time by copying the platform-specific manifest. +- Both manifests include `browser_specific_settings.gecko.id` — harmless on Chrome, required by Firefox. + +--- + +### Build System + +| File | Purpose | Tokens | +|------|---------|--------| +| `webpack.config.js` | 3 entry points, babel-loader, CSS pipeline, CopyWebpackPlugin | ~319 | +| `package.json` | Scripts, dependencies (v0.1.0 in package.json vs v1.6.1 in manifests) | ~417 | +| `tsconfig.json` | TypeScript config for IDE type-checking (not used by webpack) | ~73 | +| `tailwind.config.js` | Content paths, custom `danger`/`danger-dark` colors | ~89 | +| `postcss.config.js` | Tailwind + autoprefixer plugins | ~20 | +| `Dockerfile` | Node 20 Alpine build image | ~83 | +| `build-docker.sh` | Docker build helper (`chrome` or `ff` argument) | ~191 | + +**Webpack entry points**: +| Entry | Source | Output | +|-------|--------|--------| +| `popup` | `src/pages/popup.tsx` | `dist/js/popup.js` | +| `panel` | `src/pages/panel.tsx` | `dist/js/panel.js` | +| `sw` | `src/sw/sw.ts` | `dist/js/sw.js` | + +**Gotchas**: +- `devtool: "inline-source-map"` is set at top level and NOT removed for production builds — production bundles include source maps. +- `ts-loader` and `webpack-merge` are installed but unused. +- No `splitChunks` — React and shared deps are duplicated across `popup.js` and `panel.js`. +- CSS is injected via `style-loader` (runtime `