skeptic

TGPSKI/skeptic

Repo trust auditor in one stdlib-only Go binary.

v0.2.1 GPL-3 go1.26 229 built-in rules

skeptic scans a filesystem for the structural conditions that let one compromise cascade: mutable action refs, unsafe pull_request_target checkouts, over-permissioned automation identities, poisoned SKILL.md directives, shadowed MCP tools, lockfile and IDE-config injection. Attack-enabling conditions, not attack artifacts.

One binary is the CLI, the CI gate, the MCP server, and the loopback daemon. No container, no hosted control plane, no telemetry. go.mod has zero require entries.

binary
stdlib only — zero external deps
rules
229 built-in across 5 threat domains, plus Ed25519-signed rule packs
chains
16 behavior chains — ordered and unordered multi-step detection
decoders
12 payload schemes, entropy-bonus recursion, optional XOR brute force
identity
BFS over IAM/RBAC/OIDC — AWS, Azure, GCP, Kubernetes
correlation
per-directory, repo, content-hash, basename, git-temporal (COR-, DRIFT-)

skeptic covers the gaps between existing tools. It does not replace them, and it names them rather than claiming the whole surface.

  • not a CVE scanneruse Trivy, Grype, osv-scanner for package vulnerabilities
  • not a SAST toolno AST parsing, no dataflow taint tracking
  • not a secret scanneruse Gitleaks or trufflehog for credential leaks
  • not an EDRfiles at rest, not runtime behavior

The corpus decides what ships

testdata/proof holds 34 fixture families — one per defect class the scanner claims to cover, from ci-trust-abuse and depbot-ci-attack-chain to agentic-skills and agentic-output-injection. Two families exist to catch the opposite failure: clean-repo and negative-benign are false-positive control, sitting in the same suite as the positives.

Every report carries a ruleset_hash — sha256 of the active rule set — next to the findings. Two runs that disagree either scanned different bytes or ran different rules; there is no third explanation to argue about. That is what makes --baseline and --diff-only safe to put in front of a merge.

families
34 fixture families, one per claimed defect class
negatives
clean-repo + negative-benign scored alongside the positives
determinism
same bytes + same ruleset_hash → same report
gates
--fail-on severity and --fail-on-score 0–100 risk
waivers
sha256-pinned to file content; suppressed findings stay in the report
output
text, json, sarif, markdown — sarif uploads to code scanning
skeptic scan -p . -f json --write-baseline baseline.json
skeptic scan -p . -f json --baseline baseline.json --diff-only  # only what's new

A clean scan is not a repair

The easy way to make a scanner stop complaining is to delete the file it complained about. testdata/repair is built so that never scores as a fix. Each instance is a small repository carrying exactly one trust-boundary defect, and tools/score-instance.sh passes a candidate patch only on a conjunction: the targeted findings are gone, exploit.sh confirms the attack path is closed independently of the scanner, behavior.sh confirms the repo still does what it was for, nothing new appears at or above the defect's severity, and no suppression artifact was added. The scorer never passes --waivers, so a waiver written by the patcher is inert.

The graders are themselves gated. tools/selftest.sh must pass before any model run: the unpatched repo must FAIL, every saint*.sh must PASS, and every liar-*.sh must FAIL — with the check that caught each liar named in the output. A suite where one saint fails or one liar slips through is broken and may not score anything.

oracle
a conjunction of checks; scanner-clean is never sufficient alone
liars
workflow-deleter, trigger-remover, waiver-adder, suppressor, behavior-breaker
saints
scripted compliant patches that must pass every check
families
gh-actions pilot: mutable-action-pin, unsafe-prt-checkout
provenance
run manifests pin the skeptic version and the fixture tree hash
harness
leather examples/15-trust-repair — skeptic owns fixtures and graders
tools/selftest.sh                                # gate: saints pass, liars fail
tools/score-instance.sh <instance> <candidate>   # JSON verdict, per check
install
go install github.com/TGPSKI/skeptic/cmd/skeptic@latest
skeptic version  # commit, go version, rule count
scan

init writes the config and XDG data dir. scan is the default subcommand.

skeptic init
skeptic scan .
skeptic scan -p . --scan-style hybrid --mode ir  # triage: show everything
gate

Presets set the defaults; explicit flags always win. Resolution order is mode < preset < config file < flags.

skeptic scan -p . --preset ci -f sarif --fail-on high > skeptic.sarif
iterate

Incremental scans skip unchanged files via an mtime + size + sha256 cache, invalidated by ruleset hash.

skeptic scan --mode developer -p . --incremental --state-cache .skeptic-state.json
skeptic waive  # sha256-pinned suppression for a reviewed finding

One step, nothing pulled

The action builds skeptic from source, scans with the ci preset, uploads SARIF to GitHub code scanning, and fails the step on policy violation. No container image, no marketplace dependency.

- uses: TGPSKI/skeptic@v0.2.1

- uses: TGPSKI/skeptic@v0.2.1       # advisory only
  with:
    scan-style: hybrid
    threat-mode: machine-identity
    fail-on: none

MCP, daemon, and an isolated corpus

skeptic mcp serves skeptic_scan_repo, skeptic_waive, and skeptic_ingest_url over stdio JSON-RPC, plus daemon bridge tools when pointed at a running skeptic serve. The daemon binds loopback with token auth by default and adds scheduled background scans behind an HTTP API.

Handing malicious files to an agent to "check" is how the agent gets poisoned. skeptic corpus keeps threat artifacts encrypted at rest with AES-256-GCM and scans them in isolation, reporting the delta against the detections that were expected — so a sample can prove a rule without ever entering an agent's context.

skeptic corpus init                  # AES-256-GCM corpus + key
skeptic corpus fetch -s ./SKILL.md   # store a malicious sample
skeptic corpus scan --learn          # scan isolated, record expected rules

Threat intel that stays auditable

skeptic ingest turns advisories, STIX, Sigma, and YARA sources into rule packs; sign-rulepack signs them with Ed25519 detached signatures. A scan can be told to refuse anything unsigned, so a rule that changes what a merge gate blocks is traceable to a key.

skeptic gen-rule-keypair --private-out signing.key.pem --public-out signing.pub.pem
skeptic sign-rulepack --rules-file ./rules.json --private-key signing.key.pem
skeptic scan --rules-file ./rules.json --rules-pubkey signing.pub.pem --require-signed-rules