cmd/scrutineer/ main entry point, flag + config wiring
internal/config/ YAML config loader (see scrutineer.sample.yaml)
internal/db/ GORM models + helpers:
db.go Repository, Scan, Skill, Finding + sibling tables
(FindingLabel, FindingNote, FindingCommunication,
FindingReference, FindingHistory), Dependency,
Package, Dependent, Advisory, Maintainer
finding_helpers.go WriteFindingField, AddFindingNote,
AddFindingCommunication, AddFindingReference,
SetFindingLabels, SeedDefaultLabels
internal/queue/ goqite wrapper, embedded sqlite schema
internal/skills/ SKILL.md parser + loader for local dirs and
remote git repos
internal/worker/ one job kind (JobSkill) and the runner plumbing:
claude.go LocalClaude runner (bare-metal)
docker.go DockerRunner (ephemeral container per scan)
clone.go git clone/fetch helpers, URL validation
skill.go doSkill: stage skill + context, invoke claude,
dispatch output to the right parser
skill_parsers.go one parser per output_kind: findings, maintainers,
packages, advisories, dependents, dependencies,
repo_metadata, verify
stream.go claude stream-json line parser
findings.go structured report parser used by output_kind=findings
metadata.go FetchPackagesByPURL helper used by the web import button
internal/web/ HTTP handlers, templates, static assets, SSE broker
server.go browser routes + handlers + template funcs
api.go skill-facing /api router + bearer-auth middleware
api_reads.go typed read endpoints (maintainers, packages,
advisories, dependents, dependencies, findings)
api_finding_writes.go PATCH/POST/PUT for finding notes, communications,
references, labels, field updates, history
finding_forms.go browser-form analogues of the api finding writes
skills_handlers.go /skills UI routes
repo_report.go markdown report export per repository
sse.go SSE broker, splits data lines per spec
cwe.go + cwe.json embedded MITRE CWE catalogue (944 entries)
models.go model pick list, swappable from config
location.go forge URL builder for source links
jsontree.go JSON-to-HTML renderer for the Data tab
templates/ html/template files
static/ theme CSS, favicon
go test ./...
The full quality sweep:
golangci-lint run --enable gocritic,gocognit,gocyclo,maintidx,dupl,mnd,unparam,ireturn,goconst,errcheck ./...
govulncheck ./...
deadcode ./...
Scans are claude-code skills on disk. Adding one is a directory drop, no Go change. See agentskills.io for the SKILL.md format.
-
Create
skills/my-skill/SKILL.mdwith YAML frontmatter (name,description, optionallicense/compatibility/metadata). Scrutineer-specific metadata keys tell the worker how to handle the output:--- name: my-skill description: Summarises the repository's build outputs. metadata: scrutineer.output_file: report.json scrutineer.output_kind: freeform ---
-
Write the body: instructions for claude. Reference the skill-facing API when the skill needs context scrutineer already holds (prior scans, maintainers, packages, etc.). See
openapi.yamlat the repo root. -
Optional:
scripts/for bundled helpers,schema.jsonfor output validation. -
Restart scrutineer; the skill loader picks it up on startup. No code change.
When a scan runs, the worker stages the skill into work/{scanID}/.claude/skills/{name}/, writes a context.json with the repo identity plus scrutineer.api_base and scrutineer.token, and invokes claude -p "Use the {name} skill...". The skill's output goes to ./report.json (or whatever output_file names) and scrutineer's parser for the declared output_kind handles it.
- New output kind (row shape not already one of
findings,maintainers,packages,advisories,dependents,dependencies,repo_metadata,freeform,verify): add aparseXOutputmethod ininternal/worker/skill_parsers.goand a case in the switch ininternal/worker/skill.go. - New API surface for skills to read: add a handler in
internal/web/api_reads.goand a route ininternal/web/api.go, then document it inopenapi.yaml.
The CWE catalogue is distilled from MITRE's CSV download:
curl -sS https://cwe.mitre.org/data/csv/1000.csv.zip | funzip > /tmp/cwe.csv
python3 -c 'import csv,json; print(json.dumps({"CWE-"+r["CWE-ID"]:{"name":r["Name"],"description":r["Description"].strip()} for r in csv.DictReader(open("/tmp/cwe.csv"))}, separators=(",",":"), sort_keys=True))' > internal/web/cwe.json
The Broker in sse.go fans events from the worker to connected browsers. Clients subscribe via GET /events?scan={id}&repo={id} (both optional). The worker publishes two event types:
scan-log: each line from a running job, pushed immediatelyscan-status: fires when a job finishes (done/failed)
Templates use hx-ext="sse" with sse-connect and sse-swap to append log lines and trigger page reloads on completion. Embedded newlines in log lines are emitted as multiple data: lines so the browser's EventSource parser reconstructs the original text.
/api is a bearer-authenticated surface that running skills call back into. Each scan gets a random token on enqueue; the worker writes it into the workspace's context.json. Middleware (apiAuth) validates the token against the active scan row and enforces that a scan only touches resources on its own repository.
See openapi.yaml at the repo root for the full surface. The triage bundled skill is the reference example.
Mutable fields on Finding (status, severity, resolution, CVE/CVSS fields, etc.) all write through db.WriteFindingField, which logs every change to FindingHistory with a source tag (tool, model_suggested, analyst). Skill writes come through the API with source=model_suggested; browser-form writes use source=analyst. Notes, communications, references, and labels are stored in sibling tables rather than blob columns.
See threatmodel.md for the full model. Key mitigations in the code:
securityHeadersmiddleware on browser routes: host header check (localhost only) +Sec-Fetch-Siteon POST/api/*skips browser CSRF but requires a per-scan bearer token (random 32-byte hex)validateGitURL: https-only,--separator,GIT_PROTOCOL_FROM_USER=0io.LimitReaderon the one remaining upstream HTTP call (10 MB cap); skills do their own fetching- Data directory created with mode
0700 SameSite=Stricton cookies