Defensive static-analysis scanner for the AI-agent execution gap.
Project description
Actenon Scan
Find where agent-controlled intent reaches consequential actions without an enforceable authority check.
Quick start
uvx actenon-scan scan .
No install, no config, no cloud account. The first run produces a blast-radius summary:
Your agent can reach 12 consequential actions without a dominating authorization check.
REPOSITORY 4 create_file, merge, delete_ref, publish_release
MONEY 3 process_refund, issue_credit, charge_card
DATA LOSS 2 delete_s3_prefix, purge_workspace
EXECUTION 2 deploy, run_migration
EGRESS 1 notify_customers
Most exposed: app/tools.py:47 process_refund()
Reachable by: @mcp.tool()
Consequence: MONEY
Guard evidence: none found on the analysed path
Model-controlled inputs: payment_intent, amount
12 findings in 340 files (0.42s)
Next:
actenon-scan explain app/tools.py:47
actenon-scan fix app/tools.py:47
Then dig deeper:
actenon-scan explain app/tools.py:47 # show the analysed execution path
actenon-scan fix app/tools.py:47 # generate a remediation diff
actenon-scan brief app/tools.py:47 # one-page report for outreach
actenon-scan scan . --format html # self-contained HTML report
actenon-scan scan . --format markdown # paste into a GitHub issue
actenon-scan scan . --format sarif # upload to GitHub code scanning
What Actenon Scan detects
Actenon Scan finds places where a model-controlled or agent-controlled parameter reaches a consequential action — a payment, a repository mutation, a file deletion, a shell command, a database write, an email send — without a dominating authority check in the analysed path.
Consequence categories detected:
| Category | Examples |
|---|---|
| REPOSITORY | PyGithub create_file/delete_file, raw GitHub REST, GitPython push/commit |
| MONEY | Stripe refunds, Braintree charges, generic payment SDK calls |
| DATA LOSS | SQL DELETE, s3.delete_objects, file deletion |
| EXECUTION | subprocess, os.system, eval/exec, container creation |
| DATABASE | psycopg2/sqlite3 execute with caller-controlled SQL |
| EGRESS | requests/httpx POST/PUT/DELETE to external URLs |
| MESSAGING | SMTP email, Slack, Twilio, SendGrid, Resend, SES |
| IDENTITY | IAM mutations, access-control changes |
| SECRETS | Secrets-manager reads |
| DEPLOYMENT | kubectl, terraform, helm |
| FILE | File writes, chmod, rename |
What it does not establish
The scanner may establish that:
- an agent or model-controlled parameter reaches a consequential action;
- no dominating authority check was found in the analysed path;
- a recognised sink is present.
The scanner does not automatically establish that:
- an attacker can externally reach the agent;
- no guard exists outside the analysed file or supported architecture;
- exploitation is practical;
- the operation is irreversible;
- the finding is a vulnerability.
How to explain a finding
actenon-scan explain path/to/file.py:42
Shows the analysed execution path: agent entry point → model-controlled inputs → execution path → guard evidence → consequence. Includes a mandatory "What this does NOT establish" section.
How to generate a fix
actenon-scan fix path/to/file.py:42 # auto-select mode, print diff
actenon-scan fix path/to/file.py:42 --mode guard # repository-native guard
actenon-scan fix path/to/file.py:42 --mode approval # framework approval
actenon-scan fix path/to/file.py:42 --mode actenon # Actenon proof verification
actenon-scan fix path/to/file.py:42 --apply # write the change
Remediation is offered in neutral order: repository-native guard first, framework-native approval second, Actenon proof verification third. Actenon is not forced into every recommendation.
How to add it to a PR
GitHub Action (sticky comment + SARIF, zero config)
# .github/workflows/actenon-scan.yml
name: actenon-scan
on:
pull_request:
push:
branches: [main]
schedule:
- cron: '0 6 * * *'
jobs:
scan:
runs-on: ubuntu-latest
permissions:
pull-requests: write # for the sticky comment
security-events: write # for SARIF upload to Security tab
contents: read
steps:
- uses: actions/checkout@v4
- uses: Actenon/actenon-scan@v1
That's it. The action:
- Scans only changed files on PRs (full tree on push/schedule)
- Posts a sticky blast-radius comment on PRs with findings (updated in place)
- Uploads SARIF to the GitHub Security tab
- Does not fail the build by default
Ready to enforce? Add fail-on: high once you've triaged your baseline:
- uses: Actenon/actenon-scan@v1
with:
fail-on: high
All inputs
| Input | Default | Description |
|---|---|---|
path |
. |
Path to scan |
fail-on |
none |
Fail at this severity (none/low/medium/high). Default: none |
config |
"" |
Path to config file |
baseline |
"" |
Path to baseline.json for known-findings suppression |
scan-scope |
auto |
changed (PR only), full (entire repo), or auto |
comment-on-pr |
true |
Post sticky blast-radius comment on PRs |
upload-sarif |
true |
Upload SARIF to Security tab |
version |
"" |
Pin scanner version (default: action's own version) |
Outputs
| Output | Description |
|---|---|
findings-count |
Total findings |
high-count |
HIGH-severity findings |
medium-count |
MEDIUM-severity findings |
low-count |
LOW-severity findings |
sarif-path |
Path to the SARIF file |
Pre-commit hook
# .pre-commit-config.yaml
repos:
- repo: https://github.com/Actenon/actenon-scan
rev: v0.8.0
hooks:
- id: actenon-scan
args: [--changed-only, HEAD]
The default workflow does not block merging solely because findings exist
unless you explicitly configure fail-on: high.
How to export reports
actenon-scan scan . --format html --output report.html # self-contained HTML
actenon-scan scan . --format markdown --output report.md # GitHub issue / PR
actenon-scan scan . --format sarif --output report.sarif # GitHub code scanning
actenon-scan scan . --format list # old linter-style list
actenon-scan scan . --format json --output report.json # machine-readable
HTML reports are self-contained (no external scripts, fonts, or assets), safe to open locally, and include the visible honesty statement: "What this scan verified / did not verify".
How to suppress or baseline known findings
# Suppress a single finding inline:
# actenon-scan: suppress REPOSITORY-MUTATION
# Baseline known findings:
actenon-scan scan . --baseline known-findings.json
Scanning a repo that contains test fixtures
If your repo contains security test fixtures — deliberately unguarded code
used to test the scanner or your own security controls — add an
.actenon-scan.json at the repo root to exclude them:
{
"_comment": "Exclude test fixtures that contain intentionally unguarded code.",
"exclude": [
"tests/benchmark/**",
"tests/corpus/**",
"tests/fixtures/**"
]
}
The file is auto-detected by actenon-scan scan . — no --config flag
needed. This repo ships one itself; see
.actenon-scan.json.
How to configure custom guards
# Register a guard by name without a config file:
actenon-scan scan . --guard my_authorize --guard require_permission
# Or use a config file:
actenon-scan scan . --config actenon-scan.json
actenon-scan init --format json # write a default config
Every claim above is machine-verified
The claims: machine-verified badge links to a CI gate
(verify-claims.yml) that fails on
every PR, push to main, and once a day if any factual claim this README
makes about the package stops being true:
- Zero runtime dependencies — scan's single most important credibility
claim (it is what lets a security team deploy scan unilaterally into a
codebase that has adopted nothing). Read from
pyproject.toml, not prose. - Install commands — every
pip installin this README is resolved against the live registry; the Python version badge is generated, not hand-edited. - The ecosystem table — rendered from the protocol's
ecosystem.yaml, never hand-edited.
If a claim drifts, the badge goes red before a human notices.
Performance
Measured against a pinned real repository, not a synthetic tree:
langchain-ai/langchain @ fa7ce76, 1,954 Python files scanned. The pin,
every figure below, and the raw crossover table live in
tests/benchmark/perf-fixture.json.
Scan time depends on your core count, so the range is stated across the machines actually measured rather than as a single number you might not be able to reproduce:
| langchain, 1,954 files | serial | default (auto) |
|---|---|---|
| 10-core Apple Silicon | 1.9s | 0.96s |
| 2–4 core CI container | ~5.2s | ~5.2s (stays serial) |
Other repositories on the 10-core host: crewai (957 files) 2.4s → 1.1s, openai-agents (539) 2.1s → 0.67s, mcp-python-sdk (586) 0.6s → 0.29s.
Why the default is not always parallel
Parallelism is not free, and it does not pay off everywhere. Measured, it is ~2.5x faster on a 10-core machine and ~10% slower on a 2–4 core CI runner, where there are no spare cores to absorb the workers.
So --jobs auto-tunes: it parallelises only at 8+ cores and 250+
files, and stays serial otherwise. The 5–7 core range is unmeasured and
therefore treated as serial — the default is the mode that is never slower,
not the one that is sometimes faster.
--jobs N always overrides the auto-tune. --jobs 1 forces serial.
Findings are identical either way; that equivalence is a test, not a claim.
Pre-commit path: --changed-only scans a 1–3 file diff in ~150ms.
The Actenon ecosystem
Scan is one of five independent repositories that together close the execution gap — the gap between upstream authorization and the execution edge that actually performs a consequential side effect.
| Repository | Role | Depends on | Packages |
|---|---|---|---|
actenon-protocol |
The neutral wire contract — what every artefact looks like on the wire | — | actenon-protocol (PyPI) · @actenon/protocol-types (npm) |
actenon-kernel |
The open verifier — defines what a valid proof is | actenon-protocol |
actenon-kernel (PyPI) |
actenon-permit |
The developer on-ramp and authority broker | actenon-kernel, actenon-protocol |
actenon-permit (PyPI) · @actenon/sdk (npm) |
actenon-scan ← you are here |
The independent static-analysis scanner | — | actenon-scan (PyPI) |
Optional: actenon-cloud — a managed control plane (source-available; see its LICENSE). Not required by any component above; every capability in this ecosystem works without it.
Scan is the only Actenon tool that should run in your CI on day one. It has zero dependencies, scans langchain in under a second (see Performance), and tells you exactly where your agent code is reaching consequential side effects without proof-bound guards — whether or not you ever adopt the rest of the ecosystem.
What this is
Scan is a defensive static-analysis scanner that detects consequential actions reachable from AI-agent tool boundaries — and checks whether they're guarded. It is:
- Independent — zero runtime dependencies (
dependencies = []inpyproject.toml). Installable without pulling in any other Actenon package. - Neutral — recognises Actenon guards AND non-Actenon guards (
authorize,check_permission,verify_proof,has_role,jwt_required,opa_eval,casbin_enforce,verify_api_key,verify_mtls, etc.). Using Actenon is not the only remedy. - Honest — importing Actenon alone does NOT make a repo "safe". A
import actenonline in an unrelated module is not a guard. Scan refuses to green-light a codebase on the basis of imports. - Adoption-aware — shows 7 remediation routes per finding, only 2 of which mention Actenon. The other 5 are framework-native or redesign routes.
- CI-native — ships as a Python package, a CLI, and a GitHub Action with SARIF output that integrates directly with the GitHub Security tab.
Why it exists
Most agent code today reaches consequential side effects — stripe.Refund.create(), os.remove(), subprocess.run(), put_user_policy(), db.execute("DROP TABLE...") — through tool wrappers that the model can call directly. Very few of those tool wrappers verify proof bound to the exact action before the side effect happens. That is the execution gap in code form.
Scan exists to make that gap visible before it ships. It does not require you to adopt Actenon to be useful — it requires you to see where your agent code can reach money movement, data destruction, deployment, access-control change, communication, provider mutation, database mutation, or identity change without an enforceable guard.
The canonical problem statement lives in actenon-kernel/docs/THE_EXECUTION_GAP.md. Scan is the local adoption tool for detecting it; conformance (in the Kernel) is the public compatibility target.
Install
pip install actenon-scan
For TypeScript/JavaScript support:
pip install "actenon-scan[typescript]"
The base install has zero runtime dependencies. TypeScript support is behind an optional extra to preserve the zero-dep property — scan's value is that it installs into a codebase that has adopted nothing.
Or use the GitHub Action (no install required) — see below.
Use
# Scan a codebase
actenon-scan scan ./my-agent-code
# See adoption guidance for each finding
actenon-scan adopt ./my-agent-code
# Register custom guard function names
actenon-scan init
# Edit actenon-scan.json, add your guard function names
# Suppress known findings with a baseline
actenon-scan scan ./my-agent-code --baseline baseline.json
Example: detecting the execution gap
Save this as refund_tool.py:
from langchain.tools import tool
import stripe
@tool
def refund(pid: str, amt: int) -> str:
"""Refund a payment."""
return stripe.Refund.create(payment_intent=pid, amount=amt)
Run the scanner:
$ actenon-scan scan .
actenon-scan: 1 finding(s) in 1 file(s) (scanned 1 file(s))
refund_tool.py
6:11 [HIGH] PAY-STRIPE-REFUND (payments)
stripe.Refund.create(payment_intent=pid, amount=amt)
confidence: high
Guard this payment call before execution. Options:
(1) add an existing internal authorization check,
(2) register it with scan --config,
(3) use Actenon Kernel proof verification,
(4) use brokered Actenon protection (Permit + adapter),
(5) redesign the boundary if this action should not be agent-reachable.
Notice the @tool decorator on line 4 — that is what makes the stripe.Refund.create() call on line 6 agent-reachable, and therefore in-scope for the scanner. The same call inside a plain function with no agent-tool boundary would be silently ignored to avoid false positives.
What Scan detects — 8 consequence categories
Scan walks the AST and finds calls to consequential / irreversible operations across eight categories. Rules are configurable in actenon_scan/rules/default_rules.json; you can add your own.
| Category | Example sinks |
|---|---|
| Payments | stripe.Refund.create(), braintree.Transaction.sale(), paypal.Payment.create() |
| Data destruction | os.remove(), shutil.rmtree(), DROP TABLE, DELETE FROM, TRUNCATE |
| Deployment | subprocess.run(), kubectl apply, terraform apply, helm install |
| Access control | put_user_policy(), attach_role_policy(), create_role(), assign_role() |
| Communication | sendmail(), slack.postMessage(), twilio.messages.create() |
| Provider SDK | github.create_issue(), boto3.delete_object(), azure.storage.delete_blob() |
| Database mutation | INSERT INTO, UPDATE, db.save(), cursor.execute("DELETE...") |
| Identity change | create_user(), assign_role(), rotate_keys(), update_permissions() |
Each finding includes: rule ID, category, severity, description, file:line:column, and the matched call text.
Detection requires an agent-tool boundary. A finding is only raised when the risky call is reachable from a recognised agent-tool boundary — a
@tool-decorated function (LangChain, LlamaIndex), an MCP@server.toolhandler, a method on a tool-base subclass, or a module that imports a supported agent framework. Standalone calls with no agent context are intentionally ignored to avoid drowning you in false positives on code that isn't agent-reachable. See the example below for what this looks like in practice.
What Scan recognises as a guard
Scan recognises three classes of guards. A sink is "guarded" if a recognised guard call appears lexically before the sink in the same function body, OR a recognised guard decorator wraps the function containing the sink. (This is a v1 lexical-precedence heuristic — documented limitation.)
30+ vendor-neutral guard patterns
Recognised out of the box, no Actenon required:
authorize,check_permission,require_permission,has_role,require_roleverify_proof,verify_token,verify_signaturejwt_required,require_jwt,validate_jwtopa_eval,casbin_enforce,cedar_authorizeverify_api_key,verify_mtls,verify_client_certrequire_auth,require_authz,auth_requiredcan,may,is_allowed,check_accessauthorize_request,authorize_action- …and the rest in
actenon_scan/detectors/guards.py
Actenon-specific guards
Recognised when you are using the Actenon ecosystem:
verify_pccb,PCCBVerifier,PCCBVerifier.verifyProtectedExecutor,ProtectedExecutor.executeActenon,Actenon.local,Actenon.cloudBroker,Broker.execute,Broker.execute_via_adapterGateway,Gateway.executeBoundaryMiddleware,BoundaryVerifier,BoundaryVerifier.verify_boundary
Custom guards
Register your own guard function names:
actenon-scan init
# Edit actenon-scan.json:
# {
# "guards": ["my_internal_check", "company_authorize", "acme_can"]
# }
actenon-scan scan ./my-agent-code
Custom guards are first-class — Scan does not privilege Actenon guards over yours.
What Scan does NOT do
This is the part that makes Scan trustworthy in a vendor-neutral CI:
- Does NOT report a repo as safe merely because Actenon is imported. An
import actenonline in an unrelated module is not a guard. Scan refuses to green-light a codebase on the basis of imports. - Does NOT report a repo as unsafe merely because a non-Actenon guard is used.
@jwt_requiredon a refund endpoint is a real guard. Scan recognises it. - Does NOT make Actenon the only remedy. Each finding ships with 7 remediation routes — only 2 mention Actenon.
- Does NOT inspect prompts, model output, or in-band response content. It is a static-analysis tool, not a runtime filter.
- Does NOT replace conformance. Scan is the local adoption tool; conformance (in the Kernel) is the public compatibility target. See
actenon-kernel/docs/EXECUTION_GAP_SCANNER.md. - Does NOT make a runtime-safety claim. A guarded sink is "lexically guarded", not "provably safe at runtime". The v1 lexical-precedence heuristic is documented in
actenon_scan/detectors/guards.py. - Does NOT do interprocedural reachability. TypeScript analysis (like Python) is single-function and lexical. A concrete worked example: LangChain's
ShellToolMiddlewareexposes an@tool-decorated shell executor whose sink is three method hops away behind a policy object (@tool shell_tool→self._run_shell_tool()→self._policy.spawn()). Scan reports no findings on that file. This is the documented single-function limitation — a named, honest limitation is worth more to a security reviewer than a vague caveat.
GitHub Action
Drop this into .github/workflows/actenon-scan.yml — no install step, no API key, no Cloud account:
name: Actenon Scan
on:
push:
branches: [main]
pull_request:
jobs:
scan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actenon/scan@v1
with:
path: ./src
fail-on: medium
The Action:
- Installs
actenon-scanfrom PyPI. - Runs the scan, emitting SARIF.
- Uploads the SARIF to the GitHub Security tab via
github/codeql-action/upload-sarif@v3. - Fails the build if any findings meet the
fail-onseverity threshold.
Inputs:
| Input | Default | Purpose |
|---|---|---|
path |
. |
Path to scan (file or directory) |
fail-on |
medium |
Fail the check when findings meet this severity |
config |
"" |
Path to a custom actenon-scan.json config |
baseline |
"" |
Path to a baseline.json for known-findings suppression |
Output formats
| Format | Flag | Use case |
|---|---|---|
pretty (default) |
--format pretty |
Human-readable terminal output |
json |
--format json --output results.json |
Machine-readable, for piping into other tools |
sarif |
--format sarif --output results.sarif |
GitHub Security tab integration |
Remediation routes — 7 per finding, only 2 mention Actenon
Each finding ships with seven remediation routes. Scan does not pretend Actenon is the only answer.
- Add an existing internal guard. If your codebase already has a guard function Scan didn't recognise, just add it via
actenon-scan initand the finding disappears. - Register the guard with Scan. Same as above — Scan treats your guards as first-class.
- Use Actenon Kernel (proof verification). For when you want proof-bound execution at the resource boundary. The Kernel is the trust anchor; it works without Permit or Cloud.
- Use brokered Actenon protection (Permit + adapter). For when you control the agent framework and want credentials never to reach the agent. The recommended developer on-ramp.
- Use resource-owned verification. For when the resource itself is the protected endpoint (FastAPI route, Express endpoint, Go handler). The Boundary Kit automates this.
- Use Cloud-managed Actenon. For when you want a hosted control plane with 9-layer evidence bundles. Optional.
- Redesign the boundary. Sometimes the right answer is to remove the consequential action from the agent's reachable tool surface entirely.
Cloud is optional. Local brokered protection (route 4) works without any Cloud login.
What's in this repo
| Component | Location |
|---|---|
| CLI entry point | actenon_scan/cli.py |
| Scan engine (AST walk + sink detection + guard check) | actenon_scan/engine.py |
| Sink detector (consequential-action rules) | actenon_scan/detectors/sinks.py |
| Guard detector (vendor-neutral + Actenon + custom) | actenon_scan/detectors/guards.py |
| Reachability analysis | actenon_scan/detectors/reachability.py |
| Default rules (8 categories) | actenon_scan/rules/default_rules.json |
| Rule loader | actenon_scan/rules/loader.py |
| Baseline (known-findings suppression) | actenon_scan/baseline.py |
| Suppression directives | actenon_scan/suppress.py |
| Pretty reporter | actenon_scan/report/pretty.py |
| JSON reporter | actenon_scan/report/json_out.py |
| SARIF reporter | actenon_scan/report/sarif.py |
| GitHub Action | action.yml |
| Security policy | SECURITY.md |
| Contributing guide | CONTRIBUTING.md |
Independence
Scan depends on nothing at runtime. No Permit, no Kernel, no Cloud, no Protocol. actenon-protocol is a dev-only dependency, used solely by the drift-gate test (tests/test_protocol_drift.py) to verify that Scan's guard vocabulary and refusal-code references stay in sync with the Protocol's catalogue — it is not installed when you pip install actenon-scan. See pyproject.toml.
Scan is a standalone security tool. You can adopt it without adopting anything else from Actenon, and you can stop using it without affecting any other Actenon component.
License
Apache-2.0 — see LICENSE.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file actenon_scan-1.0.0.tar.gz.
File metadata
- Download URL: actenon_scan-1.0.0.tar.gz
- Upload date:
- Size: 152.1 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.1.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
7f40f7a55dff9aa241a0aa845d85f8ec4fe7d4e03a0485280d03a57708d60140
|
|
| MD5 |
a217884ff25ebce023147eeb92adc129
|
|
| BLAKE2b-256 |
eeee6a86e9129c3bf629fc5fd898c7971a50f334c8d64b26a6900b62e142cacc
|
File details
Details for the file actenon_scan-1.0.0-py3-none-any.whl.
File metadata
- Download URL: actenon_scan-1.0.0-py3-none-any.whl
- Upload date:
- Size: 114.8 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.1.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
ba9923c3b0297d62f50fd4f2fa4d3600364801aa9b58124589fb718a93ccfaf1
|
|
| MD5 |
5222cd4613772f633bb6cc6325df82aa
|
|
| BLAKE2b-256 |
77460bbcdf1f5ccf787efc30bb5b646ba99c6125939b3e7608b39373a7a708f0
|