agssh

module
v1.8.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 10, 2026 License: Apache-2.0

README

Air-Gapped Static Surface Hardening (agssh)

Prove your static site makes zero unexpected network calls deterministically, on every push.

A surface, a client-side tool, a docs site, a marketing page should talk only to its own origin. But nothing stops a CDN font, a stray analytics beacon, or a wide-open CSP from quietly turning the browser into a data-exfiltration path. AGSSH-STD-001 (the Air-Gapped Static Surface Hardening standard) pins that property down, and agssh is the deterministic, fail-closed runner that enforces it against the live deployment, not a promise. Built for teams shipping static or client-side surfaces where privacy and supply-chain integrity are the product.

release ci standard license

Quick start

Point it at a live URL and read the verdict; no config file, no build step:

brew install fabriziosalmi/agssh/agssh        # or: go install github.com/fabriziosalmi/agssh/cmd/agssh@latest
agssh scan https://your-surface.example/
agssh scan -profile Silver -level L1 -allow-connect https://api.example.com https://your-surface.example/
agssh scan -json https://your-surface.example/ | jq '.score.pct'   # machine-readable; summary on stderr

agssh scan takes any number of URLs (flags and URLs may be interleaved) and exits 0 conformant · 1 non-conformant · 2 usage error · 3 unscannable, so it drops straight into a shell script or CI step. Source-plane rules (secrets, vulnerable deps, SBOM, CI) stay INCONCLUSIVE unless you add -repo, -dist, -workflows. For a declared, versioned manifest use agssh -config .airgap.yml instead (below). No Go and no Homebrew? Use the Action or the ghcr.io/fabriziosalmi/agssh image: docker run --rm ghcr.io/fabriziosalmi/agssh scan https://your-surface.example/.

You get a verdict, then a severity-ranked queue of what to fix and, kept separate, what could not be checked (fail-closed, never a silent pass):

AGSSH-STD-001 v1.8.0 — https://example.com/ @ Bronze/L0
Verdict: NON-CONFORMANT   Score: 42/86 (49%)
Rules: 7 PASS · 8 FAIL · 1 INCONCLUSIVE · 0 waived · 0 N/A

Fix queue (FAIL only, highest severity first):
  [CRITICAL] AG-NET-01 Self-host every runtime dependency
         observed: no CSP; fetch-directives fall back to the browser's wildcard-open defaults
         expected: a CSP locking every fetch-directive to 'self'/'none'
  [HIGH]     AG-CSP-01 Ship a CSP
         observed: no Content-Security-Policy
  … 6 more …
Could not assess (1 — unverified, fail-closed; supply what's missing):
  AG-SUP-04 No secrets in shipped output   (gitleaks not on PATH)

Or drop the conformant-by-default manifest into your repo and gate every push:

# .github/workflows/conformance.yml
- uses: fabriziosalmi/agssh@v1          # pin to a SHA in production
  with: { config: .airgap.yml }

Or let an agent scan a surface over MCP, in-process:

go install github.com/fabriziosalmi/agssh/cmd/agssh-mcp@latest
claude mcp add agssh -- agssh-mcp        # then: "scan https://example.com with agssh"

Four ways in (CLI, Action, container, MCP), one idea: nothing is green unless proven.

The standard

[`standard/AGSSH-STD-001-v1.8.0.pdf`](./standard/AGSSH-STD-001-v1.8.0.pdf) — 57 rules across 9 families,

browsable as RULES.md. The rules live as structured data in standard/build_pdf.py, which emits the PDF, RULES.md, and the runner's rule manifest from one source — so the document and the runner cannot drift.

9 families egress (AG-NET), CSP (AG-CSP), headers (AG-HDR), DNS (AG-DNS), supply-chain (AG-SUP), CI (AG-CI), privacy (AG-PRV), output (AG-OUT), governance (AG-GOV)
3 cumulative profiles Bronze ⊂ Silver ⊂ Gold — raise the target as the surface's value grows
3 strictness levels L0 strict air-gap · L1 scoped egress · L2 marketing
Assurance tiers header → meta → runtime → structural — how strong the evidence is, not just whether a box is ticked

Full normative text (why · check · fix · tool per rule) is in the PDF; the one-line-per-rule index is RULES.md.

The runner

A single static Go binary. Native DNS (miekg/dns) and TLS probing (crypto/tls plus a raw TLS-1.0 ClientHello for legacy-refusal) — no dig/testssl; headless verification via chromedp (no Node); signing via cosign.

Posture: fail-closed

A rule the runner cannot conclusively verify returns INCONCLUSIVE, and the gate treats that exactly like FAIL. Nothing is green unless proven — a missing scanner or a check not implemented in this build both block. The gate fails on any failing MUST, any unwaived failing SHOULD, or any governance violation. A surface whose live deployment cannot be fetched at all is reported as UNSCANNABLE (with the transport cause) and emits no verdict, score, or badge — an unreachable host can never masquerade as a low score.

Exit codes: 0 conformant · 1 non-conformant · 2 usage/internal error · 3 a surface was unscannable.

Verification planes
Plane How Needs
static live headers + HTML + CSP parse —
dns miekg/dns against a validating resolver —
tls raw-socket TLS-1.0 ClientHello (legacy refusal) + crypto/tls modern handshake —
dynamic headless Chromium: offline proof, pre-consent egress, egress canary a browser
supply gitleaks, osv-scanner; source-map / SBOM scans scanners on PATH
ci workflow static analysis (pinning, permissions, untrusted code) —
engine waiver governance, signing, hermetic-build hints cosign (Gold)

All 57 rules are registered with full metadata; implemented checkers span every plane, including branch protection via the GitHub API (AG-CI-05) and runtime service-worker / client-storage inspection (AG-NET-07 / AG-PRV-04). A handful of rules whose verification is process-specific or not yet automated in this build — reproducible-build comparison, deterministic-output diffing, report-only rollout history — are registered INCONCLUSIVE with the exact approach to wire in: they block until proven, and each is a single Checker in internal/rules.

Build & run
go build -o agssh ./cmd/agssh
./agssh -config .airgap.yml                       # evaluate the live surface(s)
./agssh -profile Gold -sign -artifact dist/bundle.tar.gz \
        -approvers approvers.txt -author "$GIT_AUTHOR"

Flags: -config -repo -dist -workflows -out -profile -level -sign -artifact -author -approvers -timeout -resolver -badge -version.

The output splits what you must fix from what could not be checked: a Fix queue (FAIL only, severity-ranked) and a Could not assess list (INCONCLUSIVE, with the reason). A degraded environment — no headless browser, a missing scanner — is called out at the top, so a partial scan never looks as confident as a complete one.

Conformance badge

-badge out.svg emits a self-hosted SVG badge — no web font, no external image, no third-party endpoint — so a project can serve it from its own origin without breaking its own AG-NET-02. It is a deterministic, pure function of the record:

  • conformant → the earned tier as its metal (AGSSH · Gold), a fail-closed claim that reproduces from the same record;
  • non-conformant → the grey target tier plus the weighted score (AGSSH · Gold · 41%) — the gap, shown for what it is.

The score is a development diagnostic (an INCONCLUSIVE is environment-sensitive and fail-closed) and is level-relative — a ratio over the rules in scope at the declared level, not comparable across levels. Compare medals, not numbers; the medal is the claim.

As a GitHub Action

Runs from a prebuilt image (ghcr.io/fabriziosalmi/agssh), so no per-run rebuild.

- uses: fabriziosalmi/agssh@v1
  id: agssh
  with: { config: .airgap.yml, sign: "true" }   # needs id-token: write for keyless cosign at Gold
- run: echo "conformant=${{ steps.agssh.outputs.conformant }} report=${{ steps.agssh.outputs.report }}"

Outputs: conformant ("true"/"false") and report (path to the JSON record). A ready-to-adapt workflow is in examples/workflow.yml.

Watching surfaces over time

examples/watch.yml is a scheduled workflow that scans a list of surfaces (examples/watch-targets.json) and opens one issue when any drops below its min_score floor or stops being reachable, then closes it when they recover. The floor is a ratchet: a still-non-conformant surface can be tracked and only alerts when it gets worse.

As an MCP server

agssh-mcp exposes the same engine over the Model Context Protocol on stdio, so an agent can scan a surface without the build/config/parse dance. It reuses the runner in-process — no shell-out, no temp files.

go install github.com/fabriziosalmi/agssh/cmd/agssh-mcp@latest
claude mcp add agssh -- agssh-mcp        # or use the repo's .mcp.json
Tool Purpose
agssh_scan Evaluate a live URL; the manifest is synthesized from the arguments (url, profile, level, kind, allow_*, …). Source-plane rules report INCONCLUSIVE.
agssh_scan_config Evaluate an existing .airgap.yml with full CLI parity — repo / dist / workflow rules against real paths.
agssh_list_rules List the rule registry, filterable by family and profile. No network.

Each scan returns a human summary and the full structured record (verdict, weighted score, severity-ranked fix queue, unassessed list, environment block). agssh_scan fetches the caller-supplied URL server-side, so it refuses loopback/private/link-local targets by default (an SSRF guard enforced at dial time on the resolved IP); pass allow_private_targets to scan a local surface.

Waiver governance (the draconian heart)

MUSTs are never waivable (AG-GOV-01); the runner enforces expiry against its own clock (AG-GOV-02); a per-surface debt ceiling caps active waivers (AG-GOV-03); at Gold each waiver is signed by an approver distinct from the author (AG-GOV-04). A valid waiver only ever suppresses a SHOULD failure. Truthfulness of a justification stays a human decision — the runner governs the mechanics only.

Repository layout

standard/   the standard itself: rendered PDF + the canonical generator (build_pdf.py)
RULES.md    browsable index of all 57 rules (generated — never drifts)
cmd/        the agssh CLI and agssh-mcp (MCP server)
internal/   runner internals (manifest, csp, rules, engine, report, mcpsrv, badge)
action.yml  GitHub Action wrapper       Dockerfile  runtime image (Chromium + cosign)
.airgap.yml conformant-by-default starter manifest   examples/  wiring the gate

Contributing

The rules are single-sourced in standard/build_pdf.py; never hand-edit the generated manifest.yaml / RULES.md / version_gen.go. See CONTRIBUTING.md for the build/test/regenerate loop and the fail-closed invariant, and SECURITY.md to report a vulnerability (a check that can be made to report a false PASS is in scope).

License

Apache License 2.0 — permissive, with an explicit patent grant. The runner and its checkers compose freely into any pipeline, and AGSSH-STD-001 stays openly implementable.

Directories

Path Synopsis
cmd
agssh command
Command agssh is the AGSSH-STD-001 conformance runner.
Command agssh is the AGSSH-STD-001 conformance runner.
agssh-mcp command
Command agssh-mcp serves the AGSSH-STD-001 runner over the Model Context Protocol on stdio.
Command agssh-mcp serves the AGSSH-STD-001 runner over the Model Context Protocol on stdio.
internal
badge
Package badge renders a self-contained SVG conformance badge from a report Record.
Package badge renders a self-contained SVG conformance badge from a report Record.
csp
Package csp parses a Content-Security-Policy string and exposes the predicates the standard relies on.
Package csp parses a Content-Security-Policy string and exposes the predicates the standard relies on.
engine
Package engine selects the applicable rules for a surface, runs their checkers, evaluates the engine-plane governance rules, applies waivers, and renders the fail-closed gate verdict into a signed conformance record.
Package engine selects the applicable rules for a surface, runs their checkers, evaluates the engine-plane governance rules, applies waivers, and renders the fail-closed gate verdict into a signed conformance record.
httpx
Package httpx provides a hardened HTTP client and a fetched-document model used by the static checks.
Package httpx provides a hardened HTTP client and a fetched-document model used by the static checks.
manifest
Package manifest parses and validates the per-repo .airgap.yml declaration: target profile, conformance level, surfaces, DNS zone, pipeline scope, the explicit allow-lists, and the governed waiver/deviation block.
Package manifest parses and validates the per-repo .airgap.yml declaration: target profile, conformance level, surfaces, DNS zone, pipeline scope, the explicit allow-lists, and the governed waiver/deviation block.
mcpsrv
Package mcpsrv exposes the AGSSH-STD-001 runner over the Model Context Protocol.
Package mcpsrv exposes the AGSSH-STD-001 runner over the Model Context Protocol.
report
Package report defines the signed conformance record (the evidence artifact) and renders a human summary.
Package report defines the signed conformance record (the evidence artifact) and renders a human summary.
rules
Package rules defines the rule model and every checker.
Package rules defines the rule model and every checker.
target
Package target holds the URL helpers shared by the entry points that synthesize a manifest from a bare URL (the `agssh scan` subcommand and the MCP server), so both normalize a target identically.
Package target holds the URL helpers shared by the entry points that synthesize a manifest from a bare URL (the `agssh scan` subcommand and the MCP server), so both normalize a target identically.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL