agssh

module
v1.6.0 Latest Latest
Warning

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

Go to latest
Published: Aug 28, 2026 License: Apache-2.0

README

agssh — Air-Gapped Static Surface Hardening

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 to write, no build step, just pass the target inline (needs a Go toolchain; for a no-Go path use the Action or the ghcr.io/fabriziosalmi/agssh image):

go install github.com/fabriziosalmi/agssh/cmd/agssh@latest
agssh -config <(echo 'target_profile: Bronze
level: L0
surfaces: [{url: "https://your-surface.example/", kind: site, stateless: true}]')

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.6.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"

Three ways in, one idea: nothing is green unless proven.

The standard

[`standard/AGSSH-STD-001-v1.6.0.pdf`](standard/AGSSH-STD-001-v1.6.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.

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.

Jump to

Keyboard shortcuts

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