| name | remediate-finding |
| description | Use when the user asks to remediate, patch, fix, or produce a code fix for a security finding from the audit campaign — creating a minimal reviewable patch on a private fork, running the repo's own build/test suite, and emitting a schema-validated remediation report. |
Remediate Finding — Automated Patching Harness (Stage 9)
You are producing a minimal, reviewable patch that closes one
security finding from the Glasswing audit campaign, on a private
fork of the affected repository. The patch is committed to a
dedicated fix branch, verified locally with the repository's own
build/test tooling, and recorded as a schema-validated
*-remediation.json report.
Authorization & embargo: Findings are embargoed (see
progress-tracker/opened-tickets.md). Patches are pushed only to
the private fork resolved by fork_map.py. NEVER push to, open a PR
against, or otherwise reference the finding on the upstream public
repository — upstream disclosure is a separate human-driven step
outside this harness.
Inputs
You are given a single manifest row from
analysis-results/remediations/_manifest/remediation-manifest.csv,
identified by rem_id (<repo>.<finding_id>). The row carries:
| Field | Meaning |
|---|
upstream_url / audited_commit | Exact source the finding's file:line references resolve against |
fork_url / fix_branch | Private fork + branch name to push the fix to |
triage_path | *-triage.json — rationale, preconditions[], confidence, owner_hint |
audit_path | *-security-audit.json — evidence[].code, remediation text, cwes[] |
validation_report / validation_verdict | Live-validation outcome (the attack plan that proved the finding) |
remediation_hint | One-line suggested fix from the audit report |
Read the triage rationale first — it is the authoritative
root-cause analysis. The audit remediation text is a suggestion;
the triage rationale tells you why the code is wrong, which is what
the patch must address.
Phase 1 — Workspace
bash harnessing/remediate-finding/run_one.sh <rem_id>
This:
- Ensures the private fork exists and is synced from upstream
(
ensure_fork.sh — creates a private mirror if missing).
- Clones a working tree at
audited_commit and creates fix_branch.
- Copies the triage + audit reports next to the worktree.
- Marks the manifest row
patching and prints a JSON workspace
descriptor (worktree, fix_branch, out, …).
Capture the printed JSON — every subsequent phase uses worktree and out.
Phase 2 — Understand
In the worktree at the audited commit:
- Open every
file:line from source_findings[].locations and the
triage first_links[]. Read enough surrounding context to
understand the data flow the rationale describes.
- Confirm the rationale still holds at this commit. If the code has
already been fixed (e.g. upstream patched it independently), set
summary.status = abandoned, write a short
notes explaining why, emit the report (Phase 5), and stop.
- Identify the single root cause the patch will close. If the
finding spans multiple independent root causes, fix only the one the
triage rationale identifies as primary and record the rest under
patch.residual_risk.
Phase 3 — Patch
Produce the smallest correct change that eliminates the root cause:
| CWE / class | Preferred strategy (patch.strategy) |
|---|
| CWE-22, 73, 78, 88, 94 (injection / path) | input-validation — reject or canonicalize before use |
| CWE-400, 770, 789 (resource exhaustion) | bound-resource — io.LimitReader, semaphore, size cap, timeout |
| CWE-918, 941 (SSRF) | url-allowlist — restrict scheme + host to a fixed set |
| CWE-284, 285, 862, 863 (authz) | rbac-scope-down (RBAC YAML) or api-restrict (CR validation/CEL) |
| CWE-306, 287 (missing auth) | auth-add — wire WithAuthenticationAndAuthorization, require client cert |
| CWE-295, 319 (TLS) | tls-enforce — drop InsecureSkipVerify, require min TLS 1.2 |
| CWE-494, 829 (untrusted content) | digest-pin — require @sha256: image refs / signature verification |
| CWE-1188, 276 (insecure default) | config-default-harden — flip the default, gate the unsafe path on an explicit opt-in |
| Vulnerable dependency | dependency-bump — minimal version bump + go.sum/Cargo.lock |
| Anything else | other — explain in rationale |
Hard constraints:
- Touch only files needed to close this finding. No drive-by
refactoring, formatting, or unrelated fixes.
- Preserve public API and CRD schema unless the finding is the API.
If a behaviour change is unavoidable, document it in
patch.behaviour_change.
- Match the surrounding code's idiom (error wrapping, logging, naming).
- Add or extend a unit test that fails before the patch and passes
after, when the repo has an obvious test pattern for the touched
package. Record it in
patch.tests_added. If no test is feasible,
say why in notes.
- For RBAC scope-downs: verify (
grep / read controller code) that the
removed verbs/resources are not used elsewhere by the same
ServiceAccount. List the verified call sites in patch.rationale.
Commit on fix_branch with message:
glasswing: fix <finding_id> — <short title>
<one-paragraph rationale>
Addresses: <triage_path>#<finding_id>
CWEs: <CWE-…>
Phase 4 — Local checks
bash harnessing/remediate-finding/run_checks.sh <worktree> <out> > <out>/checks.json
run_checks.sh auto-detects Go/Rust/Python and runs build, vet/lint,
and unit tests. Inspect any fail:
- A failure caused by the patch → fix the patch, re-commit,
re-run. Iterate until green or until you determine the failure
reveals a genuine compatibility break — then record it in
patch.behaviour_change and notes and continue.
- A failure that also occurs on the unpatched base (re-run
run_checks.sh on a clean checkout of audited_commit to confirm) →
not your fault; record it in notes and treat as pass for
status purposes.
Phase 5 — Report
python3 harnessing/remediate-finding/emit_remediation_report.py \
--rem-id <rem_id> \
--worktree <worktree> \
--checks <out>/checks.json \
--strategy <strategy> \
--rationale "<why this closes the root cause — reference the triage rationale>" \
--behaviour "<user-visible change or 'none'>" \
--residual "<anything deliberately not fixed>" \
--tests-added "<pkg/foo_test.go:TestX>"
python3 scripts/validate_report.py \
--schema schema/remediation.schema.json \
<out>/<rem_id>-remediation.json
The report MUST validate. Fix any schema or cross-check errors before
proceeding.
Phase 6 — Push & record
git -C <worktree> push fork <fix_branch>
python3 harnessing/remediate-finding/update_remediation_progress.py \
<rem_id> checks_passed --fix-commit <sha>
Do not open a PR yet — the pilot keeps fixes on branches until a
human reviews the *-remediation.json. (When instructed, a draft PR
within the private fork — base main, head <fix_branch> — is
acceptable; record it under pull_request with target: private-fork
and update status to pr_opened.)
Phase 7 — Re-validation (optional, when --revalidate)
If the manifest row has validation_verdict == confirmed and you are
asked to close the loop:
- Build the operator image from the worktree
(
make docker-build IMG=<registry>/<repo>:glasswing-<rem_id> or the
repo's documented equivalent). Record the image ref.
- Run
validate-operator-live <logical_product> with
OPERATOR_IMAGE_OVERRIDE=<image_ref> so the patched image is
installed instead of the catalog default.
- Compare the original attack-plan step's verdict before/after. The
finding is fixed iff the step that previously returned
confirmed now returns refuted.
- Populate the
revalidation block in the report
(before_verdict, after_verdict, fixed, image_ref,
validation_report_path) and update status to
revalidated_fixed or revalidated_still_vulnerable.
Output Layout
analysis-results/remediations/<logical_product>/<repo>/
├── <rem_id>-context.json # manifest row snapshot
├── <rem_id>-remediation.json # schema-validated report
├── <rem_id>-run.log # run_one.sh log
├── patch.diff # unified diff (referenced by report)
├── checks.json # run_checks.sh output
├── check-*.log # per-check logs
└── <repo>-{triage,security-audit}.json # copied for self-containment
Safety Controls
- Private-fork only:
fork_map.py resolves the push target;
ensure_fork.sh creates it private: true. The skill MUST refuse
if fork.visibility != private|internal.
- No upstream writes:
run_one.sh adds upstream as a read-only
remote; only fork has a push URL with credentials.
- Embargo: commit messages and branch names use the Glasswing
finding ID, never a CVE or public tracker reference.
- Audit trail: every state transition is recorded via
update_remediation_progress.py (fcntl-locked) and the
*-remediation.json embeds the patch sha256.