
Harden
- 43 installs
- 325 repo stars
- Updated August 2, 2026
- athola/claude-night-market
Harden is an agent skill that runs cross-cutting dependency, secret, CI/CD, container, and SBOM hardening checks against concrete repo signals.
About
Harden is an agent skill from the Claude Night Market catalog that walks solo builders through cross-cutting security checks that apply no matter which stack you use. Instead of language-specific lint rules, it focuses on dependency posture (committed lockfiles, audit steps in CI, Dependabot/Renovate, pinning, hash requirements, license enforcement), secret hygiene (gitleaks-style hooks, .env handling, long-lived CI secrets), and related pipeline and container/SBOM shape. Each check is labeled with IDs like DEP01 or SEC02 and tied to CWE or NIST-style guidance so you can turn findings into a prioritized fix list before release. It explicitly composes with supply-chain advisory blocklists where versions are security-critical. Indie teams shipping SaaS, APIs, or CLIs use it when they want a structured audit pass without adopting a full commercial scanner suite first.
- Seven dependency posture checks (DEP01–DEP07) spanning lockfiles, CI scanners, pinning, hashes, and license policy
- Secret hygiene checks (SEC01–SEC04) for pre-commit scanners, .gitignore, and CI secret exposure patterns
- Composes leyline:supply-chain-advisory for known-bad version blocking (DEP03)
- Container shape and SBOM checks alongside CI/CD chain review (cross-cutting, not language-specific)
- Detection-oriented table: each row maps ID, CWE/NIST refs, and concrete repo signals to look for
Harden by the numbers
- 43 all-time installs (skills.sh)
- Ranked #1,382 of 2,203 Security skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/athola/claude-night-market --skill hardenAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 43 |
|---|---|
| repo stars | ★ 325 |
| Last updated | August 2, 2026 |
| Repository | athola/claude-night-market ↗ |
What it does
Run language-agnostic hardening checks on lockfiles, CI scanners, secrets, containers, and SBOM posture before you ship or iterate in production.
Who is it for?
Best when you want a repeatable security posture review across polyglot repos before production cutover.
Skip if: Skip if you already enforce the same controls org-wide via a dedicated AppSec platform and only need language-specific SAST—not a markdown checklist skill.
When should I use this skill?
You need a cross-language hardening pass on dependencies, secrets, CI/CD, containers, or SBOM before ship or after infra changes.
What you get
You get a prioritized checklist of DEP/SEC-style gaps mapped to CWE and detection heuristics so you can harden the repo before merge or deploy.
- Prioritized hardening gap list keyed by check IDs
- Action items mapped to CWE/NIST-style references
By the numbers
- 7 dependency posture checks (DEP01–DEP07)
- 4 secret hygiene checks (SEC01–SEC04)
Files
Harden Codebase Skill
Active security hardening: scan the existing repository for vulnerabilities and forward-facing threats, then propose concrete remediations the user can approve, defer, or file.
This skill is the engine behind /harden. It complements the Claude Code built-in /security-review (which scans the pending diff) by sweeping the whole repository against citation-backed checks rather than line-level review of in-flight code.
When To Use
- Quarterly security-posture audits.
- Before tagging a release that touches sensitive code paths.
- After a published advisory affects the language ecosystem.
- When onboarding a new repository and want a baseline.
- After integrating a new dependency or upstream service.
When NOT To Use
- Pending-diff review on a single PR. Use
/security-review. - Architecture-level threat modeling. Use
attune:war-room
with a security-focused panel.
- Cryptographic protocol review. The skill flags suspect crypto
but does not propose protocol fixes (specialist work).
- One-off bug hunting. Use
pensive:bug-review.
Required TodoWrite Items
1. harden:discovery: inventory languages, build files, hooks, CI workflows 2. harden:scan-python: run python-checks.md detectors when Python is present 3. harden:scan-rust: run rust-checks.md detectors when Rust is present 4. harden:scan-cross-cutting: run cross-cutting.md detectors (deps, secrets, SBOM, CI) 5. harden:scan-frontier: run frontier-checks.md (PQC, LLM supply chain, sandboxing) 6. harden:nist-mapping: map findings to NIST SSDF practices 7. harden:proposals: for each finding above the threshold, draft a concrete remediation per modules/proposal-shape.md 8. harden:approval-gate: present proposals to the user for apply / file / defer / reject 9. harden:apply-and-validate: apply approved proposals as discrete commits, re-run gates, capture evidence 10. harden:findings-verified: citations confirmed by citation_verifier.py 11. harden:report: write reviews/harden-<date>.md and optionally post to Discussions
Progressive Loading
Load modules based on what the discovery step finds.
| Detected | Load |
|---|---|
Python files (*.py, pyproject.toml) | modules/python-checks.md |
Rust files (*.rs, Cargo.toml) | modules/rust-checks.md |
| Any | modules/nist-controls.md (citation backbone) |
| Any | modules/cross-cutting.md (deps, secrets, CI) |
LLM SDK use (anthropic, openai), MCP server, post-quantum surface | modules/frontier-checks.md |
| Any with proposals enabled | modules/proposal-shape.md |
The module hub keeps the SKILL.md itself under the estimated_tokens: 1100 budget. Detail lives in the modules.
Core Workflow
Phase 1: Discovery
Inventory the repo without modifying anything:
# Languages and build files
find . -type f \( -name '*.py' -o -name '*.rs' -o -name '*.sh' \) \
| head -200 > /tmp/harden-langs.txt
# Build manifests
ls pyproject.toml Cargo.toml package.json go.mod 2>/dev/null
# CI workflows and pre-commit
ls .github/workflows/ .pre-commit-config.yaml 2>/dev/null
# Hooks and Dockerfiles
find . -path ./node_modules -prune -o -type f \
\( -name 'hooks.json' -o -name 'Dockerfile*' \) -printDispatch /discovery-prefilter if the repo has > 5000 source files to bound the scan.
Phase 2: Citation-backed scan
For each detected language, load the matching module and run its detector list. Each detector outputs findings with the schema defined in modules/proposal-shape.md. The citation column is mandatory: a finding without a NIST/CWE reference is downgraded to "advisory" and not eligible for active proposal.
Phase 3: NIST mapping
Group findings by SSDF practice (PW.4, PW.8, RV.1, etc.) and CWE ID. The mapping table lives in modules/nist-controls.md. The report's executive summary references SSDF practice coverage so the audit is comparable across runs.
Phase 4: Proposal generation
For each finding above the configured severity threshold, draft a concrete remediation per modules/proposal-shape.md:
- Specific files and lines touched
- Diff or config snippet (not "consider doing X")
- Blast-radius assessment via
pensive:blast-radius - Reversal plan: how to revert if the change breaks behavior
- Test that should pass after the change
Phase 5: Approval gate
Present proposals one at a time via AskUserQuestion. Default options: apply, file as issue, defer to backlog, reject. Auto-apply is opt-in via the --auto-apply flag and respects a per-finding severity threshold.
Phase 6: Apply and validate
Apply each approved proposal as a discrete commit:
git add <touched files>
git commit -m "harden: <finding-id> <one-line summary>"After each apply, re-run the project gates:
make test --quiet && make lint && make type-checkIf a gate fails, revert the commit (git revert HEAD --no-edit) and downgrade the finding to "needs human design."
Phase 7: Report
Write reviews/harden-<date>.md with:
- Executive summary (SSDF practice coverage, CWE distribution)
- Findings table grouped by severity
- Per-finding detail: detection signal, citation, proposal, status
- Disposition table (applied / filed / deferred / rejected)
- Re-run instructions
If running inside a PR context, post the executive summary as a comment via abstract:post_review_insights.
Severity Classification
| Severity | Definition | Default disposition |
|---|---|---|
| CRITICAL | Active exploit path, RCE, credential leak | apply or file immediately |
| HIGH | Plausible exploit, missing defense-in-depth on attack surface | propose for apply |
| MEDIUM | Best-practice gap, hardening opportunity | propose for apply with --auto-apply medium |
| LOW | Style/documentation gap with security flavor | file as issue |
| ADVISORY | Pattern detected without exploit narrative | report only |
Output Format
# Hardening Report — <date>
## Executive Summary
- Codebase: <repo> @ <sha>
- Languages scanned: Python (X files), Rust (Y files)
- NIST SSDF practices covered: PW.4, PW.7, PW.8, RV.1, RV.2
- CWE Top 25 hits: <count> across <distinct CWEs>
- Disposition: <N> applied, <N> filed, <N> deferred, <N> rejected
## Findings
| ID | Severity | Citation | File:Line | Disposition |
|----|----------|----------|-----------|-------------|
| H1 | CRITICAL | CWE-502, NIST SSDF PW.7 | `src/x.py:45` | applied (commit abc123) |
| H2 | HIGH | CWE-89, NIST SSDF PW.4 | `src/y.py:120` | filed (#456) |
## Per-finding detail
### H1 — Unsafe deserialization
**Citation:** CWE-502 (Deserialization of Untrusted Data),
NIST SSDF PW.7 (Review and analyze human-readable code).
**Detection signal:**
- File: `src/x.py:45`
- Anchor: `data = pickle.loads(user_supplied_input)`
- Pattern: <module>.loads(user_supplied_input)
- Reachability: untrusted, comes from request body
**Proposal:** ...
**Blast radius:** ...
**Reversal plan:** ...Safety Rails
- Never apply without approval. Even with
--auto-apply,
CRITICAL findings always prompt.
- One finding per commit. Reversals are per-finding, not
per-batch.
- Re-run gates after each apply. A gate failure reverts the
commit and downgrades the finding.
- Citation is mandatory. Findings without a NIST/CWE/RustSec
reference are advisory only and skip the apply phase.
- Read-only on first run. First invocation defaults to
--report-only until the user has reviewed at least one report and explicitly opts into proposals.
Integration
The skill composes (rather than re-implements):
pensive:rust-review: full Rust audit when Rust is presentpensive:bug-review: bug-hunting backbonepensive:safety-critical-patterns: NASA Power-of-10 adaptedpensive:tiered-audit: three-tier discipline (--tier 1/2/3)pensive:blast-radius: change-impact assessment for proposalsleyline:supply-chain-advisory: dependency postureleyline:authentication-patterns: auth/credential reviewleyline:content-sanitization: input handlingabstract:hook-authoring: hook-event securityimbue:proof-of-work: evidence discipline for findings
Verify Findings Are Grounded (harden:findings-verified)
Every finding must cite a real location and a verbatim anchor. Write findings to .review/findings.json and confirm each citation resolves:
python plugins/imbue/scripts/citation_verifier.py \
--findings .review/findings.json --repo-root .Drop or label UNVERIFIED any finding the verifier fails (exit 1); only verified findings enter the report. See Skill(imbue:review-core) Step 5 and Skill(imbue:structured-output) for the schema.
Exit Criteria
- [ ] Discovery output lists every language and build manifest
detected in the repo.
- [ ] Each finding carries a CWE or NIST SSDF citation; the
report executive summary lists the SSDF practice coverage.
- [ ] Each finding above the severity threshold has a concrete
proposal (file, diff or config snippet, blast radius, reversal plan, expected-passing test).
- [ ] No proposal was applied without explicit user approval
(or without an --auto-apply flag covering its severity).
- [ ] Each applied proposal is its own commit, reversal-friendly.
- [ ] After every apply, the project gates were re-run; any
gate failure reverted the commit and downgraded the finding.
- [ ]
reviews/harden-<date>.mdexists and lists every finding
with a disposition (applied / filed / deferred / rejected / advisory).
- [ ] Every reported finding carries a
Location+ verbatimAnchor
confirmed by citation_verifier.py (exit 0), or unverified findings were dropped or labeled UNVERIFIED.
Cross-Cutting Hardening Checks
Checks that apply regardless of the source language: dependency posture, secret hygiene, CI/CD chain, container shape, and the SBOM.
Dependency posture (composes leyline:supply-chain-advisory)
| ID | Check | CWE / NIST | Detection |
|---|---|---|---|
| DEP01 | Lockfile committed | PW.4 | absence of uv.lock / Cargo.lock / package-lock.json |
| DEP02 | Dependency scanner runs in CI | RV.1 | no pip-audit/cargo audit/npm audit step in workflows |
| DEP03 | Known-bad versions blocked | CWE-829 | leyline:supply-chain-advisory blocklist not consulted |
| DEP04 | Auto-update bot configured | RV.1 | no dependabot.yml / renovate.json |
| DEP05 | Direct deps pinned to exact versions | CWE-494 | ^1.2 / ~1.2 / 1.2.* for security-critical deps |
| DEP06 | Hash-pinned for top-tier supply-chain trust | CWE-494 | --require-hashes not used in pip / requirements.txt |
| DEP07 | License policy enforced | none | no cargo deny license rules / no pip-licenses check |
Secret hygiene
| ID | Check | CWE | Detection |
|---|---|---|---|
| SEC01 | Pre-commit secret scanner installed | CWE-798 | no gitleaks / trufflehog / talisman in .pre-commit-config.yaml |
| SEC02 | .env files git-ignored | CWE-200 | .env tracked or unmatched in .gitignore |
| SEC03 | Long-lived secrets in CI | CWE-798 | secrets.SOME_KEY used without if: github.event_name != 'pull_request' |
| SEC04 | OIDC publishing configured | CWE-798 | PyPI/Cargo publish step uses password: rather than OIDC id-token: write |
| SEC05 | Audit trail for secret access | PW.7 | repo settings: secret-access logs not retained |
| SEC06 | Sealed-secrets / secret manager | CWE-798 | secrets baked into config files instead of fetched from a manager |
CI/CD chain (GitHub Actions example)
| ID | Check | NIST SSDF | Detection |
|---|---|---|---|
| CI01 | Third-party actions pinned by SHA, not tag | PW.4 | uses: foo/bar@v1 instead of @<full SHA> |
| CI02 | permissions: block per workflow | PW.4 | top-level permissions: missing or permissions: write-all |
| CI03 | GITHUB_TOKEN minimum scope | PW.4 | default permissions used when contents: read would suffice |
| CI04 | Concurrency cancel for stale runs | RV.2 | no concurrency.cancel-in-progress: true |
| CI05 | Workflow dispatch requires approval for protected branches | PW.4 | branch protection allows direct dispatch |
| CI06 | SLSA provenance generated for releases | RV.2 | release workflow does not invoke slsa-framework/slsa-github-generator |
| CI07 | SBOM generated and attached to releases | RV.2 | release workflow lacks cyclonedx/syft/spdx-sbom-generator step |
Container hardening (when Dockerfiles exist)
| ID | Check | CWE | Detection |
|---|---|---|---|
| CO01 | Non-root USER set | CWE-269 | USER root or USER directive missing |
| CO02 | FROM is digest-pinned | CWE-494 | FROM ubuntu:22.04 instead of FROM ubuntu@sha256:... |
| CO03 | Distroless or slim base for production | PW.4 | FROM ubuntu:latest / FROM debian:latest for runtime image |
| CO04 | Read-only root filesystem in compose | CWE-269 | read_only: true not set |
| CO05 | seccomp/apparmor profile referenced | CWE-269 | runtime config lacks profile |
| CO06 | Multi-stage build to drop build deps | CWE-665 | single-stage FROM keeps gcc, make, etc. in runtime |
| CO07 | HEALTHCHECK defined | none | no liveness signal (operational hygiene) |
SBOM and provenance
# CycloneDX SBOM for the whole repo
syft . -o cyclonedx-json > sbom.cdx.json
# SPDX SBOM (alternative format)
syft . -o spdx-json > sbom.spdx.json
# Verify against the in-toto attestation if released
cosign verify-attestation --type slsaprovenance \
--certificate-identity-regexp '.*' --certificate-oidc-issuer-regexp '.*' \
ghcr.io/<org>/<image>:<tag>The hardening report includes an SBOM-coverage row: present / absent for each release artifact in the repo.
Pre-commit security suite
The skill proposes adding (or extending) .pre-commit-config.yaml with:
repos:
- repo: https://github.com/PyCQA/bandit
rev: 1.7.10
hooks:
- id: bandit
args: ["-c", "pyproject.toml"]
additional_dependencies: ["bandit[toml]"]
- repo: https://github.com/gitleaks/gitleaks
rev: v8.21.2
hooks:
- id: gitleaks
- repo: https://github.com/Yelp/detect-secrets
rev: v1.5.0
hooks:
- id: detect-secrets
args: ["--baseline", ".secrets.baseline"]If cargo is on PATH:
- repo: local
hooks:
- id: cargo-deny
name: cargo deny
entry: cargo deny check
language: system
files: 'Cargo\.(toml|lock)$'Severity defaults
| Family | Default | Justification |
|---|---|---|
| DEP04, DEP07, CI07, CO07 | LOW | operational hygiene; no exploit narrative |
| DEP01, DEP02, SEC01, SEC02, CI02, CI03, CO01, CO02 | MEDIUM | one defense-in-depth layer missing |
| DEP03, SEC03, SEC04, CI01, CI06, CO03 | HIGH | exploitable supply-chain or privilege issue |
| SEC03 with leaked active credential | CRITICAL | active exploit path |
A finding can be promoted from default with evidence (e.g., DEP01 promoted to HIGH if the lockfile is missing AND auto-merge is enabled on dep PRs).
Frontier Hardening Checks (2025-2026)
Forward-facing checks that defend against threats just emerging in production. Findings here are usually MEDIUM by default because exploitation is non-trivial; promote to HIGH when the codebase has a high-value attack surface (auth provider, signing service, data plane).
Post-quantum migration readiness
The NSA CNSA 2.0 timeline targets quantum-resistant crypto for NSS by 2030; PCI DSS 4.0.1 expects an inventory by 2026. Most application code is not the right place to swap algorithms, but the crypto-agility posture is.
| ID | Check | Citation | Detection |
|---|---|---|---|
| PQ01 | Signing/verification has a single hard-coded algorithm | NIST IR 8547 | algorithms = ["RS256"] or algorithms = ["EdDSA"] literal in JWT/JWS code |
| PQ02 | Algorithm selection driven by config, not code | NIST IR 8547 | move the algorithm list behind a signing_algorithms config field |
| PQ03 | Inventory of crypto APIs in the repo | NIST CNSA 2.0 | no docs/crypto-inventory.md or equivalent |
| PQ04 | TLS clients accept algorithm downgrade silently | CWE-757 | requests / reqwest defaults without minimum-TLS pin |
The proposal for PQ02 is usually a small refactor: move algorithms = ["EdDSA"] into a config table the operator can override. The skill does not propose ML-DSA / Falcon migration in application code (still specialist work).
LLM and agentic supply chain
A new failure mode in 2025: AI assistants suggest dependencies that look plausible but do not exist (or are typosquats). The checks below defend the development pipeline itself.
| ID | Check | Citation | Detection |
|---|---|---|---|
| LLM01 | Index pinning to defeat dependency confusion | OWASP LLM Top 10 #08 | pyproject.toml lacks [[tool.uv.index]] priority order |
| LLM02 | New deps require human review | OWASP LLM Top 10 #08 | no CI rule blocking auto-merge on dep PRs |
| LLM03 | LLM SDK calls validate role/instruction boundaries | OWASP LLM Top 10 #01 | system prompt concatenated with user input without separator/role |
| LLM04 | Tool-use response sanitization | OWASP LLM Top 10 #02 | tool output rendered to UI/terminal without escape |
| LLM05 | MCP server allowlist of tools | OWASP LLM Top 10 #02 | MCP config mounts every tool from a server (no allowlist) |
| LLM06 | Agent action audit trail | OWASP LLM Top 10 #06 | no log of tool invocations with inputs |
Sandbox / isolation posture
For codebases that execute user-supplied or AI-supplied code:
| ID | Check | Why | Today's option |
|---|---|---|---|
| SB01 | User code runs in same process as host | host privilege escalation | Pyodide WASM (Python), wasmtime (Rust) |
| SB02 | Network egress unrestricted from sandbox | data exfiltration | gVisor egress policy, NetworkPolicy in K8s |
| SB03 | Filesystem capabilities ambient | path-based attacks | cap-std (Rust), bind-mount only required dirs |
| SB04 | Resource limits absent | DoS via runaway workload | cgroup memory.max, cpu.max; prlimit in containers |
eBPF / runtime security hooks
Production codebases benefit from runtime monitoring even when the static defenses are good. The skill flags absence:
| ID | Check | Tool | What it catches |
|---|---|---|---|
| RT01 | Runtime detection layer present | Falco / Tetragon / Tracee | unexpected syscalls, container escapes |
| RT02 | App emits structured audit events | OpenTelemetry traces with semantic conventions | post-incident reconstruction |
| RT03 | Deployment includes seccomp/apparmor profile | runtime config | exploit blast-radius capping |
Differential-privacy / PETs awareness
For codebases that handle aggregable user data (analytics, ML training, telemetry):
| ID | Check | Citation | Signal |
|---|---|---|---|
| DP01 | Aggregations expose per-user values without noise | NIST SP 800-188 | counts/means published without DP budget |
| DP02 | Logs retain raw PII beyond retention window | GDPR Art. 5 | log retention config absent or > 30 days for PII fields |
Memory-safety migration triage (when C/C++ is present)
Per CISA's "Secure by Design" pledge and the ONCD memory-safety report (Feb 2024), new code in safety-critical contexts should be in a memory-safe language by default. The skill flags the opportunity, not the migration:
| ID | Check | Signal | Proposal |
|---|---|---|---|
| MS01 | C/C++ code paths handle untrusted input | parser, network code in C/C++ | rewrite or wrap behind a Rust shim |
| MS02 | C-style string handling | strcpy, sprintf, gets | move to Rust or use safestr/absl::Cord |
Output
Findings here use the same schema as the other modules. Severity is MEDIUM by default; promote to HIGH when:
- The repo is an auth/signing/credentialing service (PQ findings)
- The repo ships an MCP server or agent harness (LLM findings)
- The repo has untrusted-code-exec posture (SB findings)
- The repo handles regulated PII (DP findings)
NIST and CWE Citation Backbone
Every finding in a hardening report carries a citation. This module is the lookup table.
NIST SSDF (SP 800-218) practice mapping
The Secure Software Development Framework defines four practice groups: Prepare the Organization (PO), Protect Software (PS), Produce Well-Secured Software (PW), Respond to Vulnerabilities (RV). Findings map to PW and RV most often.
| Practice | What it requires | Detector signal |
|---|---|---|
| PW.4 | Reuse existing well-secured software | dependencies pinned, scanned, attested |
| PW.5 | Create source code aligned with secure practices | linter enforces auth/crypto/serialization rules |
| PW.6 | Configure compilation, build processes, links | RUSTFLAGS hardening, Python -W error, reproducible builds |
| PW.7 | Review and analyze human-readable code | SAST run in CI; findings tracked |
| PW.8 | Test executable code | fuzz coverage, mutation tests, property tests |
| PW.9 | Configure software with secure default settings | yaml SafeLoader, TLS verify on, autoescape on |
| RV.1 | Identify, confirm vulnerabilities on a continuous basis | dependency scanner runs on every push |
| RV.2 | Assess, prioritize, remediate vulnerabilities | severity policy, SLA per severity |
| RV.3 | Analyze vulnerabilities to identify root causes | post-incident notes feed PW.4-9 |
The skill's executive summary lists each practice and whether the codebase has at least one detector firing for it. Coverage <80% of PW.4-PW.9 is itself a finding (RV.1 unmet).
CWE Top 25 (2024) mapping
The skill prioritizes detectors that map to the CWE Top 25 most dangerous software weaknesses. Per-finding citations name the specific CWE, not just "Top 25."
| CWE | Title | Languages most often hit |
|---|---|---|
| CWE-79 | Cross-site Scripting | Python, JS |
| CWE-787 | Out-of-bounds Write | Rust unsafe, C/C++ FFI |
| CWE-89 | SQL Injection | Python, Rust |
| CWE-352 | CSRF | Python web frameworks |
| CWE-22 | Path Traversal | All |
| CWE-125 | Out-of-bounds Read | Rust unsafe, C/C++ FFI |
| CWE-78 | OS Command Injection | Python (subprocess), shell scripts |
| CWE-416 | Use After Free | Rust unsafe, C/C++ |
| CWE-862 | Missing Authorization | Web layer |
| CWE-434 | Unrestricted File Upload | Web layer |
| CWE-94 | Code Injection | Python (eval/exec), template engines |
| CWE-20 | Improper Input Validation | All |
| CWE-77 | Command Injection | All shell-out paths |
| CWE-287 | Improper Authentication | Auth layer |
| CWE-269 | Improper Privilege Management | Container, sudo |
| CWE-502 | Deserialization of Untrusted Data | Python, Java |
| CWE-200 | Exposure of Sensitive Information | Logs, errors, telemetry |
| CWE-863 | Incorrect Authorization | Web layer |
| CWE-918 | Server-Side Request Forgery | URL fetchers |
| CWE-119 | Improper Restriction of Operations within Memory Buffer | Rust unsafe, C/C++ |
| CWE-476 | NULL Pointer Dereference | Rust unsafe, C/C++ |
| CWE-798 | Use of Hard-coded Credentials | All |
| CWE-190 | Integer Overflow or Wraparound | All |
| CWE-400 | Uncontrolled Resource Consumption | All |
| CWE-306 | Missing Authentication for Critical Function | Web/API layer |
OWASP ASVS level targets
Findings track the ASVS level they aim at:
| Level | Target | Skill default |
|---|---|---|
| L1 | Opportunistic attacker | always |
| L2 | Targeted attacker | when secrets-bearing config detected |
| L3 | Determined attacker | only on --focus all with --strict |
RustSec advisory database
For Rust findings, cite the specific advisory ID (RUSTSEC-YYYY-NNNN). The cargo audit JSON output contains these directly. Use them in the proposal's reversal plan to explain why the upgrade is required.
How findings cite
Each finding's Citation: line names:
1. Primary CWE (always) 2. NIST SSDF practice (always; pick the closest match) 3. Optional: OWASP ASVS section, RustSec ID, PEP number, CVE
Example:
Citation: CWE-502 (Deserialization of Untrusted Data),
NIST SSDF PW.7 (Review and analyze human-readable code),
OWASP ASVS V5.5 (Deserialization Prevention).A finding with no primary CWE cannot be classified above ADVISORY severity.
Proposal Shape
Every proposed remediation in a hardening report follows this schema. The schema is not optional: a finding without a complete proposal cannot be applied (the user can still choose to file or defer it).
Required fields
| Field | Purpose |
|---|---|
id | Stable identifier across runs (e.g., H7, PY03) |
severity | CRITICAL / HIGH / MEDIUM / LOW / ADVISORY |
citation | Primary CWE plus NIST SSDF practice (mandatory) |
file | Single file the proposal touches (multi-file proposals split into siblings) |
lines | Affected line range, e.g., 42-58 |
detection_signal | What pattern the scanner saw (in safe-to-quote form) |
proposal | One-paragraph description of the fix |
diff | Concrete diff or config snippet, not "consider doing X" |
blast_radius | low / medium / high (see below) |
reversal_plan | Exact command to revert and the conditions to reapply |
expected_test | Test path that should pass after the change |
Severity to default disposition
| Severity | Default disposition |
|---|---|
| CRITICAL | apply or file immediately; never advisory |
| HIGH | propose for apply with prompt |
| MEDIUM | propose for apply only when --auto-apply medium |
| LOW | file as issue by default |
| ADVISORY | report only; never proposed |
CRITICAL findings always prompt even under --auto-apply.
Blast radius scale
The proposal queries Skill(pensive:blast-radius) for the change-impact graph and reports one of:
| Tier | Definition |
|---|---|
| low | Single file; no public API change; no signature change; no behavior visible to callers |
| medium | Multiple files OR public API addition (new param with default, new method) OR test-only behavior change |
| high | Public API breaking change OR cross-plugin coupling OR config schema change |
High-blast-radius proposals require explicit approval even under --auto-apply. The skill warns when the radius rises above the user's current --auto-apply ceiling.
Reversal plan template
Reversal:
command: git revert <sha> --no-edit
retry condition: <when it would be safe to reapply>
evidence file: reviews/harden-<date>.md (this report)For config changes that cannot be reverted by git revert alone (e.g., a CI permission change that runs only on push):
Reversal:
command: git revert <sha> --no-edit
follow-up: re-trigger the workflow on master to confirm the
permissions are restored
evidence file: reviews/harden-<date>.mdExpected-passing test
Each proposal cites a test that should pass after the change:
- If a test already exists, name it:
tests/unit/x.py::test_y. - If a test must be added, the proposal includes the test code in
the same diff block. The test must fail against the pre-proposal code (RED) and pass after (GREEN).
A proposal without an expected test is downgraded to ADVISORY. This is the harden equivalent of Skill(imbue:proof-of-work)'s Iron Law.
Approval options (per finding)
When the approval gate fires, the user gets:
1. apply: apply the diff, commit, run gates, advance. 2. file: create a GitHub issue with the proposal body and close out the finding. 3. defer: log to .harden/backlog.md for future runs to surface again. 4. reject: record a rejection with optional rationale; the finding will not surface again unless code changes invalidate the rejection.
The auto-apply ceiling determines which severities skip the gate entirely:
/harden --auto-apply low # apply LOW automatically
/harden --auto-apply medium # apply LOW + MEDIUM automatically
/harden --auto-apply high # apply LOW + MEDIUM + HIGH automaticallyCRITICAL is never auto-applied.
Worked example
id: PY01
severity: HIGH
citation: "CWE-502 (Deserialization of Untrusted Data), NIST SSDF PW.7"
file: src/api/loader.py
lines: 42-44
detection_signal: |
Module imports the Python stdlib unsafe-deserialization helper
and calls its loads() helper on bytes that originate from the
request body (data flows from request.body through validate()
into loader.loads()).
proposal: |
Replace the unsafe loader call with json.loads. The payload
shape is JSON-compatible per the API spec (verified by reading
the OpenAPI schema for /v1/upload). This eliminates the
arbitrary-code-execution attack path while preserving the
positive-path behavior.
diff: |
--- a/src/api/loader.py
+++ b/src/api/loader.py
@@ -42,3 +42,5 @@
-from <stdlib-unsafe-loader> import loads
+import json
- obj = loads(payload)
+ obj = json.loads(payload)
blast_radius: low
reversal_plan:
command: "git revert <sha> --no-edit"
retry_condition: "do not retry; the previous form was unsafe by design"
evidence_file: "reviews/harden-2026-05-10.md"
expected_test: tests/unit/api/test_loader.py::test_round_trip_jsonMulti-file proposals
When a hardening fix needs touches across multiple files (e.g., adding a Tier Literal requires updates in classifiers, the DORAMetrics dataclass, and the tests), split into sibling proposals (PY01a, PY01b, PY01c) with a shared parent_id. The approval gate applies them as one unit, but each is its own commit so reverts stay surgical.
What a proposal must NOT do
- Modify generated code, vendored code, or
node_modules. - Touch the changelog except to add a
### Securitybullet. - Bump dependency versions beyond the minimum required for the
fix (a separate proposal per dep upgrade).
- Introduce a new dependency without an
imbue:proof-of-work
evidence trail showing the dep was vetted.
- Disable existing tests or assertions to make the fix easier.
Python Hardening Checks
Detectors for Python codebases. Each row is a discrete check the skill runs; each carries a CWE citation for the report.
The detection-signal column uses placeholder syntax for patterns that the project's pre-commit security hooks block writing literally. The hooks exist for a reason: this doc describes the rules, but the regexes themselves live in code where the safety review treats them as data, not source. If a future contributor needs to inspect the literal patterns, see the corresponding detector module under plugins/pensive/skills/harden/detectors/ (future work; not part of the v1 skill).
Detection ruleset
| ID | Check | CWE | NIST SSDF | Detection signal |
|---|---|---|---|---|
| PY01 | Unsafe deserialization (the stdlib pickle family, marshal, shelve) | CWE-502 | PW.5.1 | <unsafe-loader>.loads( on bytes you do not control |
| PY02 | YAML loaded without a safe Loader | CWE-502 | PW.9 | yaml.load( without Loader=SafeLoader |
| PY03 | Code injection via eval/exec/compile(...,'exec') | CWE-94 | PW.5.1 | <eval-family>( with a non-constant argument |
| PY04 | Shell-command injection in a child-process call | CWE-78 | PW.5.1 | bandit B602 / B605: child-process helper invoked with the shell-mode flag and a formatted string |
| PY05 | SQL injection via string formatting | CWE-89 | PW.5.1 | cursor.execute(f"...") or % formatting in a query |
| PY06 | Path traversal in user-supplied paths | CWE-22 | PW.5.1 | open(user_input) without Path.resolve() and is_relative_to() |
| PY07 | Insecure RNG used for security purposes | CWE-330 | PW.5.1 | import random then a token/secret/key generated from it; should be secrets |
| PY08 | TLS verification disabled | CWE-295 | PW.9 | requests.*(verify=False), ssl.CERT_NONE, disable_warnings(InsecureRequestWarning) |
| PY09 | Hardcoded credentials | CWE-798 | PW.5.1 | regex match for api[_-]?key, secret, token, passwd literals; AWS prefixes (AKIA...) |
| PY10 | Subprocess invocation without timeout | CWE-400 | PW.5.1 | <child-proc>.run/Popen without timeout= |
| PY11 | Tarfile extraction without member filter | CWE-22 | PW.9 | tarfile.*extractall( without filter= (PEP 706; default became safe in 3.12+) |
| PY12 | XML XXE / billion-laughs | CWE-611, CWE-776 | PW.9 | xml.etree.ElementTree.parse( (use defusedxml) |
| PY13 | Jinja autoescape off in HTML context | CWE-79 | PW.9 | Environment(autoescape=False) or Markup(user_input) |
| PY14 | Logging secrets | CWE-532 | PW.5.1 | logger.*(token), logger.*(password), logger.*(api_key) |
| PY15 | Bare except: swallowing errors in security paths | CWE-754 | PW.7 | except: or except Exception: pass in auth or crypto paths |
| PY16 | Async TOCTOU (time-of-check / time-of-use) | CWE-367 | PW.5.1 | await is_authorized(...) then await act(...) without re-check |
| PY17 | Untrusted format string | CWE-134 | PW.5.1 | (user_input).format(, f-string built from user input, "%" % user |
| PY18 | Insecure temp file | CWE-377 | PW.5.1 | tempfile.mktemp( (use mkstemp or NamedTemporaryFile) |
| PY19 | assert for runtime auth check | CWE-617 | PW.5.1 | assert user.is_admin (assert is stripped under python -O) |
| PY20 | requests without timeout | CWE-400 | PW.5.1 | requests.get/post(...) without timeout= |
| PY21 | HTTP request smuggling on outdated frameworks | CWE-444 | RV.1 | gunicorn<22.0.0 (CVE-2024-1135), aiohttp<3.9.2 (CVE-2024-23829) |
| PY22 | Multipart resource exhaustion | CWE-400 | RV.1 | python-multipart<0.0.18 (CVE-2024-47874); FastAPI route without max_part_size |
| PY23 | Weak hashing for passwords or auth tokens | CWE-327, CWE-328 | PW.5.1 | hashlib.md5, hashlib.sha1 reachable from password / session-token paths |
| PY24 | Missing TLS 1.2 floor | CWE-326 | PW.9 | ssl.PROTOCOL_TLSv1, PROTOCOL_SSLv23 without minimum_version |
| PY25 | Index pinning missing for PyPI consumption | CWE-829 | PW.4 | no [[tool.uv.index]] in pyproject.toml; no --index-url constraint in requirements.txt |
| PY26 | Hash-unpinned installs | CWE-494, NIST SI-7 | PW.4 | requirements.txt without --hash= lines; missing uv.lock/poetry.lock |
| PY27 | PyPI release without PEP 740 attestation | NIST SSDF PS.2.1 | PS.2 | release workflow uses static API token instead of pypa/gh-action-pypi-publish@release/v1 (Trusted Publishers) |
Library substitution table
When a finding fires, the proposal usually swaps the offending library or call. The substitution table:
| Bad | Good | Why |
|---|---|---|
| the unsafe-deserialization stdlib family | json if data is JSON-shaped, otherwise msgspec / pydantic | safe-by-default deserialization |
yaml.load(...) | yaml.safe_load(...) (or ruamel.yaml.YAML(typ='safe')) | rejects arbitrary tag construction |
eval, exec | ast.literal_eval for constants; otherwise refactor to a typed parser | no code path executes user input |
random.{choice,token_bytes,...} for security | secrets.token_bytes/urlsafe, secrets.choice | CSPRNG-backed |
xml.etree.ElementTree on untrusted input | defusedxml.ElementTree | XXE / billion-laughs hardened |
urllib.request.urlopen | requests (timeout=, verify=True) or httpx (defaults are safer) | timeout-by-default |
tempfile.mktemp | tempfile.NamedTemporaryFile(delete=False) | atomic creation |
assert is_authorized() | explicit if not is_authorized(): raise PermissionError(...) | survives python -O |
hashlib.md5/sha1 for passwords | argon2-cffi, bcrypt, or passlib | KDF with cost factor |
| static API tokens for PyPI | PyPI Trusted Publishers and sigstore attestation (PEP 740) | revocable, log-traceable, no shared secret |
Static-analyzer integration
Run the canonical Python security tools and treat their findings as first-class harden findings:
# Bandit ruleset (PyCQA-maintained AST scanner)
uv run bandit -r src/ -f json -o /tmp/harden-bandit.json
# pip-audit on the lockfile (PyPA + Trail of Bits, OSV-backed)
uv run pip-audit --lockfile uv.lock --format json > /tmp/harden-pipaudit.json
# osv-scanner reads pyproject + lockfiles + Cargo.lock
osv-scanner --format json . > /tmp/harden-osv.json
# semgrep with the security-audit ruleset
semgrep --config=p/python --json --output=/tmp/harden-semgrep.jsonFindings from these tools convert to the harden schema by joining on file:line and severity. A bandit B301 finding (the unsafe deserialization rule) becomes the same finding shape as the internal PY01 detector, so the report does not double-count.
Frontier Python concerns (2025-2026)
Loaded from frontier-checks.md for full coverage. Brief list:
- PEP 740 sigstore attestations on PyPI (verify before install);
see Trail of Bits' "Attestations: a new generation of signatures on PyPI" (Nov 2024).
- Tarfile member filter (PEP 706):
tarfile.data_filterdefault
in Python 3.12+; verify the codebase does not still pass filter=None.
- pyproject.toml
[[tool.uv.index]]priority pinning to defeat
dependency confusion attacks.
- LLM SDK prompt-injection patterns and MCP server hardening
(CWE-1426; OWASP LLM Top 10 #01, #02).
- ASGI middleware request smuggling (CVE-2024-23829 class).
- Sandboxing application code: WASM (Pyodide), gVisor, nsjail.
- Slopsquatting / package hallucination (arXiv 2406.10279):
LLM-suggested non-existent packages later registered as malware. Mitigation: lockfile and --require-hashes and human review on every dep addition.
Output schema
Each Python finding follows the schema in modules/proposal-shape.md. The detection-signal text uses safe placeholders so the doc itself does not trip secret-scanners or the project's security pre-commit hooks; the actual scanner emits the literal pattern with file:line context in the report.
Rust Hardening Checks
Detectors for Rust codebases. The Rust ecosystem ships strong default safety; the harden skill verifies the discipline is actually applied and the supply chain is curated.
For a deep ownership/unsafe audit, the skill defers to Skill(pensive:rust-review). This module focuses on the hardening posture (capability use, supply-chain hygiene, side-channel defense) and frontier 2025-2026 practices.
Detection ruleset
| ID | Check | CWE / RustSec | NIST SSDF | Detection signal |
|---|---|---|---|---|
| RS01 | Crate root lacks #![forbid(unsafe_code)] and does not declare audited unsafe | CWE-119 | PW.5 | lib.rs/main.rs without forbid; no audit/unsafe.md |
| RS02 | Unsafe block without // SAFETY: comment | CWE-119 | PW.7 | unsafe { not preceded by // SAFETY: line |
| RS03 | unwrap() / expect() on caller-supplied data | CWE-754 | PW.5 | .unwrap() after parse(), from_str, serde::from_* on external input |
| RS04 | panic! reachable from request handler | CWE-754 | PW.5 | panic!, unimplemented!, unreachable! on user-input branches |
| RS05 | cargo audit not wired to CI | RV.1 | RV.1 | no cargo audit step in .github/workflows/*.yml |
| RS06 | cargo deny config missing or not enforced | RV.1 | PW.4 | no deny.toml or CI step missing |
| RS07 | cargo vet audits stale / missing for new deps | RV.2 | PW.4 | supply-chain/audits.toml does not cover new entries in Cargo.lock |
| RS08 | Comparing secrets with == | CWE-208 | PW.5 | == between &[u8] typed as token/secret/digest; should be subtle::ConstantTimeEq |
| RS09 | Secrets not zeroized on drop | CWE-316 | PW.5 | secret-typed struct without Zeroize/ZeroizeOnDrop |
| RS10 | Async cancellation un-safe | CWE-362 | PW.5 | tokio::select! arms with non-cancel-safe futures (e.g., BufReader::read_to_end) |
| RS11 | Mutex::lock().unwrap() in hot path | CWE-754 | PW.5 | poisoned-lock unwrap reachable from public API |
| RS12 | Source replacement to private registry without integrity pin | CWE-494 | PW.4 | [source.crates-io] replace-with without checksum |
| RS13 | Git dependency without rev= | CWE-494 | PW.4 | git = "..." without rev = "..." (mutable target) |
| RS14 | Build script (build.rs) reads files outside OUT_DIR | CWE-829 | PW.6 | build.rs opens path containing .. |
| RS15 | serde(deny_unknown_fields) missing on auth-bearing structs | CWE-1287 | PW.5 | struct deriving Deserialize for an auth/config payload without #[serde(deny_unknown_fields)] |
| RS16 | Compiler hardening flags absent | CWE-1244 | PW.6 | .cargo/config.toml missing RUSTFLAGS for stack protection / CFI |
| RS17 | unsafe impl Send/Sync without proof | CWE-362 | PW.7 | `unsafe impl (Send |
| RS18 | FFI without bounds annotation | CWE-787 | PW.5 | extern "C" function with *const T / *mut T arg with no length param |
Tooling integration
# Vulnerability scan against RustSec advisory DB
cargo audit --json > /tmp/harden-cargo-audit.json
# Policy enforcement (license, advisory, source, ban list)
cargo deny check --format json 2>/tmp/harden-deny.json
# Cryptographic-supply-chain audit
cargo vet check 2>&1 | tee /tmp/harden-vet.log
# Unsafe block inventory (third-party tool; requires install)
cargo geiger --output-format json > /tmp/harden-geiger.json
# Mutation testing (high-leverage; expensive)
cargo mutants --in-diff origin/main..HEAD --no-times --jsonThe harden report joins these on file:line and advisory ID.
Capability-style hardening (frontier 2025-2026)
| From | To | Why |
|---|---|---|
std::fs ambient access | cap-std::fs::Dir capability | filesystem ops require an explicit handle, not a path |
raw socket/std::net | cap-std::net | network endpoints are capabilities, not strings |
| environment-driven config | secrecy::SecretString for secrets | wrapper prevents Debug/Display leaks |
| ad-hoc retry loops | tower::retry with backoff and budget | bounded resource consumption (CWE-400) |
These are recommendations rather than blocking findings: surface in the report as MEDIUM advisories with the rationale in the proposal.
Concurrency hardening
| Pattern | Replace with | Reason |
|---|---|---|
naked std::sync::Mutex in hot path | parking_lot::Mutex (no poisoning, smaller, faster) | reduces unwrap-on-poison footguns |
ad-hoc Arc<Mutex<HashMap>> | dashmap::DashMap | lock-free reads, sharded writes |
tokio::sync::Mutex held across await | parking_lot::Mutex for short critical sections | avoid tokio scheduler stalls |
loom-untested concurrent code | wire cargo test --features loom for lock-free types | model-checks all interleavings |
Compiler hardening flags
In .cargo/config.toml:
[target.'cfg(all())']
rustflags = [
# Stack-smashing protection
"-C", "force-frame-pointers=yes",
# Disable sources of UB
"-D", "warnings",
# Treat unsafe-op-without-unsafe-fn as error in 2024 edition
"-D", "unsafe_op_in_unsafe_fn",
]For sanitizer-instrumented test runs:
RUSTFLAGS="-Z sanitizer=address" cargo +nightly test --target x86_64-unknown-linux-gnuOutput schema
Same as python-checks.md. Each Rust finding emits a single proposal with diff, blast radius, reversal plan, and expected test. RustSec IDs go in the citation column when applicable.
Related skills
How it compares
Structured cross-cutting audit tables—not a single-language linter or a one-click hosted scanner product.
FAQ
Who is harden for?
Developers and small teams shipping SaaS, APIs, or CLIs who need dependency, secret, and CI chain checks without a full enterprise GRC stack.
When should I use harden?
During Ship security review before release, when wiring Build integrations and CI, and during Operate infra iteration after dependency or workflow changes.
Is harden safe to install?
It is procedural guidance for reviewing your repo; review the Security Audits panel on this Prism page before trusting any third-party skill package in your agent.