Autofix
cyscan fix rewrites source files in place using each rule's fix: block. It's deterministic, span-based, and backs up the originals by default.
When it works
Autofix is a literal span replacement — the fix: text is spliced over the exact range the matcher reported. Works well for:
- Swapping one API for another (
hashlib.md5→hashlib.sha256) - Flipping a config value (
InsecureSkipVerify: true→false) - Replacing a leaked secret with an env-var lookup
It doesn't work for rules whose fix: would need to preserve captured arguments — e.g. pickle.loads(data) → json.loads(data) needs data carried across. These are deferred to a later release (metavariables).
Commands
# See what would change — write nothing
cyscan fix . --dry-run
# Apply every fix, .cyscan-bak backups
cyscan fix .
# Review one by one
cyscan fix . --interactive
# No backups
cyscan fix . --no-backup
# Mirror a CI gate
cyscan fix . --fail-on high # exit 1 if a high+ finding remains unfixed
What a run looks like
$ cyscan fix tests/fixtures --dry-run
--- a/tests/fixtures/bad.go
+++ b/tests/fixtures/bad.go
@@ -8,3 +8,3 @@
- cfg := &tls.Config{InsecureSkipVerify: true}
+ cfg := &tls.Config{InsecureSkipVerify: false}
--- a/tests/fixtures/bad.py
+++ b/tests/fixtures/bad.py
@@ -3,2 +3,2 @@
-AWS_ACCESS_KEY = "AKIA1234567890ABCDEF"
+AWS_ACCESS_KEY = os.getenv("AWS_ACCESS_KEY_ID")
Without --dry-run you'd see:
fix: 2 patched, 5 fixed, 2 skipped (no fix), 0 skipped (overlap), 0 aborted; 0 findings remain
Safety rails
Descending-offset splicing
Findings are grouped by file, then sorted descending by start offset so earlier edits don't shift later ones. You can't produce a garbled file by fixing multiple findings in one go.
Overlap resolution
If two findings target overlapping ranges, the higher-severity one wins. The other is logged and skipped — the report shows it under "skipped (overlap)".
File-changed-since-scan guard
Before writing, cyscan re-reads the file and hashes it. If the content differs from what the scanner saw, the file is aborted with an error:
ERROR file modified since scan; re-run cyscan before fixing: app/auth.py
This stops stale scan output from corrupting files that have since moved on.
.cyscan-bak backups
By default, each patched file's original is saved alongside it:
app/
├── auth.py ← patched
└── auth.py.cyscan-bak ← original
Roll back with:
mv app/auth.py.cyscan-bak app/auth.py
Disable with --no-backup when your workflow is "review the diff in git" instead.
Interactive mode
cyscan fix . --interactive
For each finding, you see the diff and a prompt:
Apply this fix? [y]es / [n]o / [s]kip file / [q]uit:
y/ Enter — applyn— skip this finding, continue the files— skip the rest of this file, move on to the nextq— abort the whole run
Using in CI
- name: Apply autofixes
run: |
cyscan fix . --no-backup
if ! git diff --quiet; then
git config user.name "cyscan-bot"
git config user.email "bot@example.com"
git add -A
git commit -m "cyscan: apply autofixes"
git push
fi
For pull-request-based workflows, open a PR rather than push to main:
- uses: peter-evans/create-pull-request@v6
with:
commit-message: "cyscan: apply autofixes"
branch: "cyscan/autofix"
title: "Cyscan autofixes"
Authoring fix: blocks
From Writing rules:
id: CBR-PY-WEAK-HASH-MD5-SHA1
regex: |
\bhashlib\.(md5|sha1)\b
fix: 'hashlib.sha256'
The fix: string replaces exactly the matched range. Backwards-compatible — rules without a fix: are silently skipped by cyscan fix.