Skip to main content

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.md5hashlib.sha256)
  • Flipping a config value (InsecureSkipVerify: truefalse)
  • 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 — apply
  • n — skip this finding, continue the file
  • s — skip the rest of this file, move on to the next
  • q — 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.

Next step