Skip to content

Releases: ImalkaPerera/deviant

v0.3.2

Choose a tag to compare

@ImalkaPerera ImalkaPerera released this 21 Oct 13:32
99a7810

A reliability release. No change to results on ordinary projects: the JSON output is identical to 0.3.1 on 20 real repos.

Hangs fixed

  • A small workflow file of nested YAML merge keys (<<:) could hang the scan. Such a file is now skipped with a message.
  • Routers mounted twice at each level of a deep chain took exponential time. Now linear.

Crashes fixed

  • A commit dated past the year 9999 crashed every --with-git scan.
  • On Python 3.10 and 3.11, a null byte in one route's annotation made the scan lose every route in the project. One bad file now skips only itself.

Wrong or missing results fixed

  • A form feed in a Python file made a # public endpoint comment above a route be read from the wrong line.
  • "auth_words": "word" as a string now works, and a broken .deviant/config.json is reported instead of ignored.
  • Repositories using SHA-256 commit IDs now get git evidence.
  • continue-on-error: ${{true}} without spaces is now recognised.
  • A pinned-action finding no longer points at an earlier step that mentions the same action.

Full details: #2

v0.3.1

Choose a tag to compare

@ImalkaPerera ImalkaPerera released this 21 Oct 06:37

What is new since 0.3.0

Fail a CI job on a finding. deviant-scan --fail-on-findings exits 1 when the scan reports a finding. Leads from --inventory do not count, a usage error still exits 2, and without the flag the exit status is 0, so adding it to a job cannot break the job by surprise. docs/using-in-ci.md explains the statuses and gives a workflow with pinned actions.

A fix found by checking against real bugs. In a group where nearly every item follows the rule, the exceptions are now listed as leads however many there are. Before, a fixed limit of 8 hid them: in a project where 174 of 199 actions were pinned, the 25 that were not never appeared. The limit still applies below 85% agreement.

Measured on real bugs, not planted ones. The scanner alone, with no AI, was run on the commit just before each of 35 fixes from public history. It pointed at 25 (22 before the fix above): all 13 unpinned actions, all 3 missing timeouts, 8 of 12 missing permissions blocks, and 1 of 7 missing-auth bugs. The missing-auth result is low for a reason the docs now state: a bug that every similar item shares has no protected sibling to compare with, so this method cannot see it. See bench/results/real-bugs.md, and bench/real_bug_recall.py to reproduce it.

Tests and docs. Property-based tests now check, over thousands of generated inputs, that results do not depend on the order items arrive in, that the text escaping leaves no control characters, and that neither detector crashes on arbitrary bytes or text. The docs record a run of the Claude skill against a repository built to give it instructions: it followed none of them.

Benchmarks, stated plainly

On a sealed fresh set of 99 planted bugs, Claude reading the code alone found 45% and Claude with the Deviant skill found 35%. Precision is not measured. See bench/results/fresh-set-benchmark.md.

v0.3.0

Choose a tag to compare

@ImalkaPerera ImalkaPerera released this 21 Oct 02:06

What is new since 0.2.0

Leads for small groups. --inventory lists every peer group and the items that lack something most of their peers have, with no size floor. It also compares an item with its neighbours in the same file. The Claude Code skill no longer stops on an empty scan: it judges these leads too. Shell steps with continue-on-error: true are now checked.

Security. A scanned repository can no longer run a program through a git filter, however the filter is named (.gitattributes, .git/info/attributes, core.attributesFile, or a redirecting .git file). Control characters from a scanned project are shown as visible escapes in the text output. Every string in the output is cut to 500 characters, and git evidence has a 60 second budget. The scan prints a line when its git history comes from outside the scanned folder.

Robustness. Several patterns that grew with the square of the input are now linear. A junction loop is walked once instead of for minutes. An alias bomb or recursive anchor in one workflow no longer slows or drops the others. Shell steps get exact line numbers from the YAML positions. Paths the system cannot read are reported, not skipped silently.

Release process. The release workflow now runs lint and the tests, and checks that the tagged commit is on main, before it builds or publishes.

Benchmarks, stated plainly

On a sealed fresh set of 99 planted bugs, Claude reading the code alone found 45% and Claude with the Deviant skill found 35%. The skill is ahead on ignored step failures and removed auth checks, and finds none of the inverted conditions that no detector checks. Precision is not measured. See bench/results/fresh-set-benchmark.md.

v0.2.0

Choose a tag to compare

@ImalkaPerera ImalkaPerera released this 20 Oct 02:27

What changed

Detector behaviour in this release:

  • FastAPI auth: auth inside a dependency's body (a 401 or 403 raised by a repo function or class) now counts, as do guard decorators such as @requires_login, role checkers, and cookie parameters.
  • Route forms: websocket routes, add_route, add_websocket_route, call-form registration, a bare Depends(), lambda endpoints, keyword include_router(router=...), aliased and star imports, and aliases inside string annotations are now followed.
  • Public paths: /public is a public path. A root route under a public mount is public, and a mount prefix does not hide a route's own path.
  • Monorepos: imports resolve to the service that owns the importing file.
  • GitHub Actions: jobs named on, yes, no or null keep their names. timeout-minutes: 0 is no longer a timeout. Comments no longer move step line numbers. Skipped workflow files are reported on stderr.
  • Robustness: a detector calling sys.exit or printing no longer breaks the scan or --json output. Malformed context is rejected per detector.

Full details are in the commit history from v0.1.1, and in docs/detectors.md.

v0.1.1

Choose a tag to compare

@ImalkaPerera ImalkaPerera released this 18 Oct 00:48

Security

git blame in --with-git could run a program named in the scanned repository's own .git/config (a textconv driver). Scanning an untrusted repository with --with-git on 0.1.0 could therefore run code on your machine. 0.1.1 runs git blame with --no-textconv. If you scan code you do not trust with --with-git, upgrade to 0.1.1. See the security advisory for details.

Fixes

  • Git evidence ignores GIT_DIR and related variables inherited from the environment, so running from a git hook no longer reads another repository.
  • A .deviant/config.json that is a symlink or over 1 MB is ignored.
  • GitHub Actions: a job whose name repeats an earlier nested key (such as a matrix variable) is reported on its own line.

Improvements

  • FastAPI: routers stored on self inside a class are followed.
  • FastAPI: a helper name counts as an auth check only if every function with that name checks credentials.

Other

  • Licensed under MIT or Apache-2.0, at your option.
  • Deviant's own CI and release jobs now have timeouts.

v0.1.0: first release

Choose a tag to compare

@ImalkaPerera ImalkaPerera released this 17 Oct 03:51

First public release of Deviant: it finds the one thing in your code that breaks the pattern.

Install

pip install deviant-scan
deviant-scan path/to/your/project --with-git

Or run it once without installing: uvx deviant-scan path/to/your/project --with-git.

In Claude Code, to have Claude judge each exception:

/plugin install deviant --marketplace ImalkaPerera/deviant

What is in it

  • FastAPI routes: finds a route missing the auth, rate limit or response model its neighbours
    have. Auth is followed through routers, shared dependencies and include_router.
  • GitHub Actions: finds an action not pinned to a commit, a job with no timeout, and a workflow
    with no permissions block or concurrency setting, in workflows and composite actions.
  • Git evidence for each exception: who last changed it, when, why, and how many of the
    protected items that person wrote.
  • A Claude Code skill that labels each exception as a bug, intentional or unsure, with proof.
  • Coloured output in a terminal, JSON with --json, and support for Python 3.10 to 3.14.

What to know

  • This is an early release. It has been run on seven real open-source projects, with the results
    read by hand. At the default settings it reported one candidate, which turned out to be
    intentional.
  • There is no confirmed bug in someone else's project yet, and no accuracy number. Both are on the
    roadmap.
  • Small projects will often show nothing: a rule needs roughly 25 similar items before one
    exception is strong evidence.

Full story, including what the early versions got wrong:
tested on real projects.