Contribute to ASH¶
This page covers a development checkout, the layout of the repository, and what a pull request goes through before it merges. The other contributor pages are Test your change, Test the documentation, and Releases and versions.
To report a bug or ask for a feature, open an issue on GitHub. To report a security issue, do not open a public issue: follow SECURITY.md.
Set up a development checkout¶
You need git, uv, and Python 3.10 to 3.14. uv downloads a Python in that range if none is installed.
-
Fork the repository on GitHub, then clone your fork:
git clone https://github.com/<your-account>/automated-security-helper.git cd automated-security-helperReplace
<your-account>with your GitHub user name.Work in a git clone, not in an unpacked source archive. The unit suite, the docs checks, and the snapshot checks list files with
git ls-files, so they fail outside a git work tree, and git must be on yourPATH. -
Install the project, its development tools, and both optional extras:
This creates
.venvwith ASH installed in editable mode, thedevdependency group (pytest, ruff, mypy, pre-commit, mkdocs, and the rest), and the two extras that CI installs:cdk(aws-cdk-lib and cdk-nag, which the cdk-nag scanner needs) andsymbols(the tree-sitter grammars thatsymbol:suppressions use). A plainuv syncleaves both extras out, and the tests that need them then skip; the skip reason names the extra.The repository has no
.python-versionfile, so uv uses the newest installed Python in the supported range. To develop against a particular version, name it:uv sync --python 3.12 --extra cdk --extra symbols.The scanners are not project dependencies. A scan runs the Python-based ones, such as bandit, checkov, and semgrep, with
uv tool run, and finds binary ones such as grype or trivy on yourPATHor in~/.ash/bin, whereash dependencies installputs them. See Manage scanner tools. -
Check that the ASH in your checkout runs:
It prints the version in
pyproject.toml.
Operating system notes¶
-
Windows: some test data paths are long. Enable long paths in git before you clone:
Git for Windows and MSYS2 ship a shell named
ash.uv run ashfinds the project's ASH first, becauseuv runputs the project's environment at the front ofPATH; outsideuv run, theashv3alias runs the same entry point. - CI runs the unit suite on Linux (x86_64 and arm64), macOS on Apple silicon, and Windows, each with Python 3.10 to 3.14, so any of these works for development. - Some tests need programs that are not Python packages, such as a container runtime, a sandbox backend, or Nix. Test your change says which, and how they are selected.
Devfile environments¶
An IDE that reads devfiles can use devfile.yaml at the root. Its install command
installs uv into ~/.local/bin and runs uv sync, build runs uv build, and test
runs uv run pytest. Each command puts ~/.local/bin on PATH itself, so it also runs
under a POSIX sh. That uv sync installs neither extra. To run the commands by hand,
copy them from devfile.yaml or
DEVELOPMENT.md.
Run ASH from your checkout¶
uv run runs a command in the checkout's environment, so uv run ash is the ASH you are
editing. With no arguments, ash scans the current directory, which inside the checkout
is the ASH repository itself. To scan another project with your checkout's code, run
from that project and point uv at the checkout with uv run --project:
cd ~/src/my-project
uv run --project ~/src/automated-security-helper ash --source-dir . --output-dir .ash/ash_output
uv run --project keeps your working directory. uv run --directory also uses the
checkout's environment but first changes into the checkout, so relative paths then
resolve inside the checkout. To run the MCP server from your checkout, see
Test an MCP change locally. Every command and
option is in the CLI reference.
Project layout¶
| Directory | What it holds |
|---|---|
automated_security_helper/ |
The package: the CLI and MCP server (cli/), configuration models (config/), the scan phases (core/), plugin base classes (base/), built-in and community plugins (plugin_modules/), result models (models/), JSON schemas (schemas/), and shared utilities (utils/) |
tests/ |
Unit, integration, and snapshot tests, test data, and test helpers; see Test your change |
docs/ |
docs/content/ is this site, built with mkdocs from mkdocs.yml |
scripts/ |
Generators for the reference pages, the docs checks, the version template manager, and the verification scripts some CI jobs run |
.github/ |
Workflows, composite actions (actions/), the gate scripts CI runs (scripts/), the changelog template, and curated release notes |
ash-agent-plugins/ |
The agent and IDE plugins: the shared sources in agentic-coding/transpiler/_base/, the transpiler that renders them, and the generated per-platform plugin trees |
skills/ |
The ash-mcp agent skill, generated by the transpiler |
deploy/ |
The CDK app, CDK constructs, and Terraform for running ASH on AWS |
examples/ |
Example CI configurations and an example plugin package |
.ash/ |
The ASH configuration that CI uses to scan this repository |
Formula/ |
The Homebrew formula |
nix/ |
Nix packages for the scanners nixpkgs does not carry; flake.nix at the root defines the development shell |
utils/ |
ash_helpers.sh, which you source from a shell profile to get an invoke-ash command that runs this checkout's ./ash from any directory; ash_helpers.ps1, which defines Invoke-ASH, the container wrapper for PowerShell; and older per-scanner scripts that ASH does not run |
Open a pull request¶
- For anything beyond a small fix, open an issue first so the change can be discussed before you spend time on it.
- Create a branch in your fork from the latest
main, and keep the change to one purpose. A reformat of unrelated code makes the change hard to review. - Add or update tests for the change, and run them as described in Test your change.
- Update the docs the change affects. Docs are checked against the code, so a page that
names an option that no longer exists fails CI; see
Test the documentation. If users will see different behavior,
describe it under
## UnreleasedinCHANGELOG.md(see Releases and versions). - Open the pull request against
mainwith a title in the commit message format below, and fill in the template's checklist.
A pull request merges when it has one approving review, every review conversation is
resolved, and the required checks pass (see the table below). The "Protect main"
ruleset also requires the branch to be up to date with main, and it includes a code
quality rule set to every severity. A push after an approval dismisses it. Merging goes
through GitHub's merge queue, which squashes the pull request into one commit and runs
the required checks again on the result.
Pull requests from forks run with a read-only token and without repository secrets, so a few steps that need them, such as the scan's comment on the pull request, are skipped. GitHub can also hold the workflow runs of a first-time contributor until a maintainer approves them.
Commit messages and pull request titles¶
Commit messages and pull request titles follow Conventional Commits, as checked by commitizen:
The type is one of build, bump, chore, ci, docs, feat, fix, perf,
refactor, revert, style, and test. A scope cannot contain whitespace, and !
marks a breaking change. For example:
feat(reporters): add a JUnit reporter option for suite names
fix(sandbox): keep the scanner cache writable under bwrap
docs: rewrite the contributor pages
feat!: drop the v2 configuration format
The Validate PR title check runs cz check on the pull request title. The squash
merge takes its subject from the pull request title when the pull request has several
commits, and from the commit itself when it has one, and the title check does not see
that commit. So give a single commit a conventional subject too; the commit-msg hook
below checks it for you.
The squash commit's body keeps every commit message, so trailers such as
Snapshot-Update: survive the merge.
The type of the subject that lands on main decides what the release does with it:
Subject on main |
Changelog | Version bump |
|---|---|---|
feat |
listed under Feat | minor |
fix, refactor, perf |
listed under Fix, Refactor, or Perf | patch |
any type with !, or a BREAKING CHANGE: footer |
listed | major, capped to minor on the 3.x line |
build, bump, chore, ci, docs, revert, style, test |
not listed | none |
So a change to CI only is ci: .... Written as fix(ci): ..., it is listed under Fix
and makes the next release a patch release. Releases and versions has
the details.
What CI runs on a pull request¶
Three checks are required to merge: "ash / SAST, SCA, and IaC Scan", Validate PR title,
and required-checks. required-checks stands for every job in the "ASH - Unified CI"
workflow, so all of those have to pass too. The repository also requires the code
scanning results that the ASH scan uploads, so a pull request that introduces an alert
does not merge.
| Check | What it checks | Required |
|---|---|---|
| ash / SAST, SCA, and IaC Scan | ASH scans this repository with the configuration in .ash/, fails on actionable findings, and posts a summary on the pull request; the sarif job uploads the SARIF report to code scanning |
yes |
Validate PR title |
the pull request title passes cz check |
yes |
lint |
the JSON schemas match a regeneration; the docs checks pass; every CI artifact upload and cache is allowlisted; every cached content database is time-bounded; and ruff check, ruff format --check, and mypy pass. ty runs for information only |
through required-checks |
actionlint |
actionlint and shellcheck over every workflow | through required-checks |
snapshot-trailers |
every changed golden file has a Snapshot-Update: trailer, and every snapshot belongs to a test module |
through required-checks |
unit-test (<os>, py<version>) |
the unit and snapshot suite with both extras, on five runners and Python 3.10 to 3.14, plus the cdk-nag integration tests. A test that cannot find an extra fails rather than skipping, and the leg fails when total coverage of automated_security_helper/ is under 80% |
through required-checks |
integration-test (ubuntu-latest) |
the integration suite with pytest --run-integration --run-slow, and the examples in the docs |
through required-checks |
build-docs |
mkdocs build --strict, and scripts/docs_versions.py check on the versioned site |
through required-checks |
converter-integration (<os>) |
the converter integration tests on macOS and Windows | through required-checks |
scan (<method>, <os>) |
real scans by every way of running ASH: local, the Python container launcher, ./ash, PowerShell, each container runtime, offline, and with the community plugins |
through required-checks |
external-target scan (<os>), multi-project attribution (<os>) |
a scan of a directory outside the repository; a workspace scan that keeps findings with their projects | through required-checks |
sandbox escape (<os>), sandbox scanner parity (<sandbox>, <os>) |
the sandbox backends contain a scanner; each scanner gives the same results with and without a sandbox | through required-checks |
deploy-tree AWS helpers (moto server) |
the helper scripts in deploy/, against a local moto server |
through required-checks |
<method> (<os>, py<version>) |
each install method (the pip, pipx, uvx, pre-commit, and MCP bundle, Homebrew, and the container runtimes) on each runner and Python it supports. The bundle runs ASH's pre-commit hooks against fixture repositories on one leg per operating system. The Homebrew legs, on Linux x86_64 and macOS with Python 3.12, install the formula built from the pull request's commit, run brew test, and scan with the installed ASH |
through required-checks |
required-checks |
fails when any Unified CI job failed or was canceled; when all of them pass it is skipped, which GitHub counts as passing | yes |
Build wheel and sdist, and assert their contents |
the package builds and contains what it should | no |
upgrade (<from>) |
the tree installs into a clean environment, over the latest release, and over the previous commit | no |
dependency-review |
no new dependency with a high-severity advisory | no |
Assert every action is pinned to a commit sha, Assert no boolean workflow input is treated as a string |
workflow hygiene | no |
cdk template drift, generated buildspec drift, terraform hygiene, cdk-nag |
the committed IaC in deploy/ matches a fresh synth, and passes its own checks |
no |
| TypeScript CI, Agent Plugins Drift, CDK Extra Drift, Install Method Validation, Docs CI Runners | the TypeScript packages in deploy/, the generated agent plugins, the cdk extra fallback, the Nix install, and the CI examples in the docs, each under its CI system's own runner (see Run the CI examples with their runners); each runs only when a pull request changes its files |
no |
A check that is not required does not block the merge when it fails, but the failure still shows on the pull request.
Scheduled checks¶
The "ASH - Pinned Tool Versions" workflow runs every Monday and can be started by hand. It
runs scripts/check_pinned_tool_versions.py --fail-on-outdated --markdown and writes the
report to the job summary. The script compares:
- each version pinned in
automated_security_helper/utils/tool_downloads.py(the release binaries, the Python scanners the container image installs, the license-only entries, and the cfn-nag gem) with its upstream's latest release on GitHub, PyPI, or RubyGems; - each
package=versionpin on anapt-get installline in theDockerfilewith the Debian and NodeSource package indexes, for amd64 and arm64.
The script fetches only HTTPS URLs on api.github.com, pypi.org, rubygems.org,
deb.debian.org, and deb.nodesource.com, and checks every redirect hop against the same
rule before it follows it. With GITHUB_TOKEN or GH_TOKEN set, it sends the token to
the GitHub API only, carries it across a redirect only to the same scheme, host, and port,
and retries a GitHub lookup that gets 403 once without the token. It exits 2 when a
lookup fails, the pins cannot be read, an apt-get install line names a package without
a version, or an apt-get install line pins a package to a different version than another
stage does, and 1 when no package index carries an apt pin's version. A pin that
is behind makes it exit 1 only with check_pinned_tool_versions.py --fail-on-outdated, as
the workflow runs it; otherwise it exits 0. To run it locally, from a checkout:
The workflow opens no issue or pull request. A bump changes every digest, the license
files, and the source commit in tool_downloads.py, so a person makes it; the report
lists each value a bump has to change.
Code style¶
CI runs these, and so can you:
uv run ruff check .
uv run ruff format --check .
uv run --group dev --extra cdk mypy automated_security_helper/
uv run ruff format . and uv run ruff check --fix . fix what they can. The ruff
settings, including the line length of 88, are in [tool.ruff] in pyproject.toml,
which also requires ruff 0.16.10 or later: an older ruff exits with code 2 rather than
lint with different rules. tests/test_data/ is excluded, because those files are
scanner input. mypy checks the package, not the tests or scripts. Its per-module
overrides in pyproject.toml are a baseline that only shrinks: fix a new type error
rather than adding the module to the overrides.
Pre-commit hooks¶
The repository's .pre-commit-config.yaml runs ruff, formats JSON files, regenerates the
JSON schemas, and keeps the cdk extra fallback in sync on each commit. The fallback sync
runs its script on Python 3.13 outside the project environment
(uv run --no-project), so it leaves your .venv and its extras as they are. The config
also checks the commit message with commitizen. pre-commit install on its own installs
only the pre-commit stage, not the commit message check. Install both, then run the
hooks over the whole tree:
uv run pre-commit install --hook-type pre-commit --hook-type commit-msg
uv run pre-commit run --all-files
Run pre-commit through uv run, so it uses the project's Python; the pinned commitizen
hook needs Python 3.10 or later. The first commit takes longer while pre-commit builds
the hook environments, and the schema regeneration runs on every commit.
On a clean checkout, pre-commit run --all-files passes and changes nothing. The JSON
formatter skips the files that a tool writes or a check compares byte for byte: the
generated agent plugin trees, _base/mcp.json that they copy, and the vendored schemas
under ash-agent-plugins/, the synthesized CDK templates, the npm manifests in
deploy/, and the snapshot files. Change those with the tool that owns them. It also
skips the editor settings under .vscode/, and deploy/cdk/tsconfig.json, which has
comments. CI runs ruff and the other checks directly rather than through these hooks,
because the ruff and JSON hooks fix what they find instead of reporting it.
The config also has a manual hook that scans the repository with your checkout's ASH:
These hooks are for developing ASH. To run ASH as a pre-commit hook in your own repository, see Run ASH with pre-commit.