AppSec Rules Pack
A versioned AppSec rules pack and the validator that keeps it honest. Each rule states
what to review, what evidence proves it, how to fix it, and how long an exception may
last, with mappings to OWASP ASVS 5.0, the OWASP API Security Top 10, OWASP Top 10:2025,
CWE, and NIST SSDF. The appsec-rules CLI validates packs against a JSON Schema contract
and semantic checks, and checks review records, the per-service outcome of a review,
against the pack's own exception policy. A CI gate reads both reports.
The demo is real CLI output. Recreate it from a source
checkout with python docs/assets/record-cli-demo.py.
Contents
- Scope
- Quick start
- Installation
- Verify a release
- CLI reference
- Writing a rules pack
- Recording a review
- Use it in CI
- Optional executable Semgrep rules
- Project layout
- Documentation
- Contributing, security, and license
Scope
| It does | It does not |
|---|---|
| Define a JSON Schema contract for AppSec review rules | Scan application code or execute rules |
| Ship a generic baseline of 29 rules, each with a compliant and a violating example | Claim compliance: mappings are review aids, not conformance |
| Validate packs: schema, duplicate IDs, exception windows and policy, mapping formats, rule lifecycle, sensitive values | Embed enforcement: CI consumes the JSON report and decides (ADR-0004) |
| Derive a rule index, a Semgrep metadata scaffold, a SARIF rule catalog, and a mapping coverage report | Turn the derived scaffold into detections: its patterns are placeholders (ADR-0001) |
Check a review record against the pack: every rule accounted for, evidence for met, exceptions allowed, complete, unexpired, and inside the window |
Confirm the evidence is true, or decide what may stay open: the record is self-declared and the gate decides (ADR-0006) |
The baseline covers authentication including federated sign-in and token issuance, password storage, object- and function-level authorization, a second approver for high-risk actions, input validation, injection (including argument injection) and XSS, unsafe deserialization, SSRF, secrets, file handling, sensitive data in storage and caches, logging and an audit trail for sensitive records, dependency risk, configuration including CORS, session hardening, CSRF, webhook integrity, excessive data exposure, mass assignment, open redirect, rate limiting, and two risks of LLM features: prompt injection, and tools or retrieval that reach beyond the caller's permissions. It contains no product names, tenant identifiers, customer data, secrets, or environment-specific configuration.
Two areas are out of the baseline on purpose. Privacy obligations such as consent and lawful basis depend on the jurisdiction, so they belong in a privacy pack; the baseline keeps only the security side, retention of sensitive data. CI/CD pipelines, infrastructure as code, and container images are a different subject from the application: review them with a policy tool built for them, such as the OSS Security Policy as Code Starter Kit, or with your own pack next to the baseline.
Mapping coverage of the baseline: CWE and NIST SSDF on 29 of 29 rules; ASVS 5.0 on 28 of
29 (ASVS 5.0.0 has no requirement for LLM prompts, so APPSEC-LLM-001 maps to the OWASP Top
10 for LLM Applications instead); the optional OWASP API Top 10 2023 on 23, OWASP Top 10:2025
on 26, and OWASP Top 10 for LLM Applications 2025 on the 2 LLM rules.
Quick start
pip install "appsec-rules-pack==0.9.0"
curl -LO https://github.com/lucashgrifoni/AppSec-Rules-Pack/releases/download/v0.9.0/appsec-baseline.yaml
appsec-rules validate appsec-baseline.yaml --require-examples --fail-on-warnings
Expected output: Validation passed: 1 file, 29 rules, 0 errors, 0 warnings.
Install into a virtual environment (python -m venv .venv, then activate it). In Windows
PowerShell, curl is an alias for Invoke-WebRequest; download the baseline with
Invoke-WebRequest <url> -OutFile appsec-baseline.yaml instead.
Installation
pip install "appsec-rules-pack==0.9.0"
This installs the appsec-rules console script and requires Python 3.11 or newer. Pin a
reviewed version, such as appsec-rules-pack==0.9.0, when the CLI runs
as a quality gate.
The distribution contains the validator, the CLI, and the JSON Schema. It does not contain
a rules pack. Take the baseline from the assets of the
release you pin, copy
rules/appsec-baseline.yaml, or write your own pack.
To work from source:
python -m venv .venv
.venv/bin/python -m pip install -e ".[dev]" # Windows: .venv\Scripts\python
Verify a release
Releases from v0.2.0 on are built and published by
publish-pypi.yml through PyPI Trusted Publishing,
with no long-lived token. From v0.3.1, each GitHub Release carries the wheel, the sdist, a
CycloneDX SBOM (sbom.cdx.json), and the baseline pack, all four with a SLSA
build-provenance attestation; from v0.4.0 it also carries the signed provenance bundle
(appsec-rules-pack-<tag>.intoto.jsonl). From v0.5.0, a release is published only from a
tag on main that matches the package version and passes the tests, and the files are
built with hash-pinned tools in a job that cannot publish. v0.1.0 was published by hand and
has no attestation. All release tags are signed.
Verify a downloaded asset with a current GitHub CLI:
gh attestation verify appsec-baseline.yaml \
--repo lucashgrifoni/AppSec-Rules-Pack \
--signer-workflow lucashgrifoni/AppSec-Rules-Pack/.github/workflows/publish-pypi.yml \
--source-ref refs/tags/v0.9.0
Or offline, against the bundle attached to the release:
gh attestation verify appsec-baseline.yaml \
--repo lucashgrifoni/AppSec-Rules-Pack \
--bundle appsec-rules-pack-v0.9.0.intoto.jsonl
The same commands work for the wheel, the sdist, and sbom.cdx.json. A passing check proves
the artifact came from this repository's release workflow at that tag. It does not prove the
rule guidance is complete or that an application is secure. Treat a failed or unavailable
check as unverified provenance. ADR-0003 explains the
release design.
CLI reference
| Command | Purpose |
|---|---|
appsec-rules init <file> |
Write a starter pack that passes the strict gate; --id and --prefix set the pack id and rule id prefix |
appsec-rules validate <file-or-dir>... |
Validate packs, or every .yaml/.yml pack under directories (linked directories are not entered); duplicate rule ids are checked across all of them |
appsec-rules init-review <pack> <record> --subject <text> --reviewer <text> |
Write a review record that lists every enabled rule, to fill in; --subject-ref, --force |
appsec-rules review <pack> <record> |
Check a review record against a pack; --as-of, --fail-on-warnings, --format json |
appsec-rules export index <pack> [-o file] |
Derive a machine-readable rule index (JSON) |
appsec-rules export semgrep <pack> [-o file] |
Derive a Semgrep scaffold with rule metadata and placeholder patterns |
appsec-rules export sarif <pack> [-o file] |
Derive a SARIF 2.1.0 rule catalog with an empty results array |
appsec-rules report coverage <pack> [-f json] [-o file] |
Count framework mappings per rule and AppSec category |
appsec-rules --version |
Print the installed version |
validate options:
| Option | Effect |
|---|---|
--fail-on-warnings |
Exit non-zero on warnings as well as errors |
--require-examples |
Warn when an enabled rule has no compliant/violating example |
--format json |
Emit a JSON report for CI |
The JSON report is identified by "schema": "appsec-rules-validation/v1" and described by
validation-report.schema.json.
It has a summary object (files, rules, errors, warnings, ok) and a files array
of per-file issues. Each issue has a level, a stable code, the rule_id it belongs to
(or null), a path inside the pack, the line and column of that path in the file (or
null), and a message. The text output shows the same location after the message. Match
on code, not on the message
text, which may be reworded. Validation exits 0 on a passing pack, 1 on a failing one,
and 2 on a usage error such as a missing path. VERSIONING.md lists
the codes and what counts as a breaking change.
The export and coverage commands only derive metadata; they do not validate first. Run
validate --require-examples --fail-on-warnings as the gate. From a source checkout,
python -m appsec_rules_pack works in place of appsec-rules.
Writing a rules pack
A pack is a pack block plus one or more rules. This minimal pack is complete and passes
the strict gate. appsec-rules init my-pack.yaml writes it for you; then validate it and
grow it. Every field shown is required except schema_version and examples, which the
strict gate (--require-examples) expects anyway. The full contract is
appsec-rule.schema.json, and the
29 rules in rules/appsec-baseline.yaml are worked examples.
pack:
id: my-pack
name: My Rules Pack
version: 0.1.0
schema_version: "0.5"
mode: advisory
owner: appsec
description: A minimal rules pack to start from.
rules:
- id: MYPACK-ERRORS-001
title: Require controlled error handling
description: Verify that invalid user input returns controlled errors without stack traces.
severity: medium
category: configuration
status: enabled
enforcement: advisory
targets:
- api
mappings:
owasp_asvs:
- V16.5.1
cwe:
- CWE-209
nist_ssdf:
- PW.7
evidence:
required:
- Error handling path returns a documented response shape.
signals:
- Negative tests assert controlled error messages for invalid input.
match:
type: review
includes:
- API handlers that process untrusted input.
excludes:
- Local developer-only scripts that are not shipped.
remediation:
guidance: Replace raw exception output with a controlled error contract.
validation:
- Run negative tests for invalid input and malformed requests.
exceptions:
allowed: true
max_days: 30
required_fields:
- owner
- justification
- expires_at
examples:
compliant:
language: python
snippet: |
except ValueError:
log.exception("order parse failed")
return {"error": "invalid order"}, 400
explanation: The client gets a fixed message; the detail stays in server logs.
violating:
language: python
snippet: |
except ValueError as error:
return {"error": repr(error)}, 500
explanation: The raw exception reaches the client and can expose internal details.
Rules files must be UTF-8 YAML without aliases or duplicate keys, and no larger than 10 MiB; the validator rejects anything else before checking the schema.
docs/rule-fields.md lists every field, its limits and allowed
values, and how to adapt the baseline: keep it as published next to your own pack, or
fork it. Shapes that are easy to get wrong: evidence and match are objects, not lists;
remediation needs guidance; required_fields accepts only owner, justification,
expires_at, compensating_control, and validation_plan.
Rule ids follow PREFIX-AREA-NNN, so an organisation can use its own prefix (ACME-AUTH-001)
next to the baseline's APPSEC- rules. CWE and NIST SSDF mappings are required; ASVS,
owasp_top_10_2025, owasp_api_top_10_2023, and owasp_llm_top_10_2025 are optional. Keys starting with x- are
allowed on the pack, on each rule, and inside mappings for your own metadata. A pack may
declare the schema it targets with pack.schema_version: "0.5".
Rules are advisory by default. Each one carries a stable ID and severity, a target surface
and category, framework mappings, expected evidence and review signals, match guidance,
remediation and validation steps, exception requirements, a lifecycle status
(deprecated rules carry a deprecation block), and a compliant and a violating example.
pack.mode and each rule's enforcement (advisory, audit, blocking) are policy
metadata for whoever consumes the pack. They never change the exit code of validate, which
only judges whether the pack itself is well formed.
CONTRIBUTING.md describes the severity model and the topic-based
mapping convention.
Recording a review
A pack states a policy. A review record states what one review found against it: for one
subject (a service, a repository, a release), each rule is met, not-met,
not-applicable, or excepted.
review:
pack: appsec-baseline
pack_version: 0.9.0
subject: payments-api
subject_ref: v3.2.0
reviewer: appsec-team
date: 2026-10-01
results:
- rule: APPSEC-AUTHZ-001
status: met
evidence:
- tests/test_authz.py::test_other_tenant_payment_returns_404
- rule: APPSEC-LOG-001
status: not-met
notes: Refund failures log the card holder name.
- rule: APPSEC-RATELIMIT-001
status: excepted
exception:
owner: payments-team
justification: Per-client quotas need a new store; planned for 3.3.
expires_at: 2026-12-15
compensating_control: Gateway limit and an alert on 429 spikes.
appsec-rules review appsec-baseline.yaml payments-api-review.yaml --format json
This record covers three rules, so every other baseline rule comes back unreviewed with a
review-missing-result warning, and the command still exits 0. In CI, add
--fail-on-warnings so a record with gaps fails the job.
review checks the record against the pack: the record names the right pack, each rule
exists and appears once, met cites evidence, and each exception is allowed by the rule,
has the fields the rule requires, has not expired, and fits in the rule's max_days.
Enabled rules with no result are reported as unreviewed, and a not-met or
not-applicable rule without notes gets a review-justification-missing warning.
review.subject_ref is optional and names the exact revision reviewed (a commit, tag,
image digest, or package version). A not-met or excepted result may set
assessed_severity when the finding is more or less severe than the rule: the report
then shows it as that rule's effective_severity and counts it in
summary.open_by_effective_severity, while severity and summary.open_by_severity keep
the rule's value (ADR-0009). Keys starting with x- on a result are copied into
that rule's entry in the JSON report.
To start a record, appsec-rules init-review appsec-baseline.yaml review.yaml --subject payments-api --reviewer appsec-team writes one result per enabled rule, each not-met
with no notes. Every rule counts as open and warns until it is filled in, so the record
cannot pass --fail-on-warnings before the review is done.
For not-met, evidence is optional. Use it to point to the failing code, test,
or observation so another reviewer can locate the defect. It does not change the status.
The exit code says whether the record is valid, nothing more. A valid record can still
have open rules. The JSON report ("schema": "appsec-rules-review/v1", described by
review-report.schema.json)
lists every rule with its severity, enforcement, and status, and counts open rules
(not-met or unreviewed) by both, so the gate can apply your policy. The record format is
review-record.schema.json.
examples/review/ has a complete record against the
baseline.
All 29 baseline rules have enforcement: advisory. A gate that checks only
summary.open_by_enforcement.blocking would allow every open baseline rule.
Gate on summary.open_by_severity instead, with the severity threshold your team
requires. For example, reject a report when its critical or high count is non-zero,
after checking that the command exited 0 and summary.ok is true. The worked review
has one open medium rule. To gate by enforcement, fork the pack and raise selected
rules to blocking; the field reference explains the field
and adapting the baseline explains the fork.
Count maps omit zero entries; in Python, read a missing severity with
summary["open_by_severity"].get("high", 0).
When you upgrade the baseline, review your records in the same change. A new enabled
rule has no result in an existing record, so it shows as unreviewed with a
review-missing-result warning, and a record that declares the old pack_version also
gets review-pack-version-mismatch. With --fail-on-warnings, both fail the CI job until
the record gets a result for each new rule and the new pack_version. The changelog lists
added rules under each release.
Use it in CI
examples/README.mdhas a GitHub Actions job that installs a pinned release, downloads and verifies the baseline, validates your packs, checks your review records, and gates on open rules, plus a PowerShell version.examples/validation_gate.pyis a stdlib-only gate for any other CI system. It passes only when the CLI exits0and the report saysok: true..github/workflows/policy-gate.ymlis this repository's reference gate that consumes the JSON report.
Optional executable Semgrep rules
exports/semgrep-rules/ holds three hand-maintained,
tested Semgrep rules, kept separate from the validator
(ADR-0005):
| Baseline rule | Detection |
|---|---|
APPSEC-INJECT-001 |
Flask query/form values reaching SQL arguments on locally created sqlite3 connections and cursors |
APPSEC-SSRF-001 |
Flask query/form values reaching the URL argument of module-level Requests calls |
APPSEC-DESER-001 |
Flask request bodies, files, and query/form values reaching pickle, marshal, or yaml.load without a safe loader |
The other 26 baseline rules have no executable detection, and these three cover only their documented source and sink combinations. The layer README lists the known false positives and false negatives. Run the fixture suite with a separately installed engine:
python -m pip install "semgrep==1.179.0"
semgrep --test --strict --metrics=off --config exports/semgrep-rules/rules exports/semgrep-rules/tests
CI runs this on Linux. Use Linux, macOS, or WSL: on native Windows, semgrep --test did
not run these fixtures in a check on 2026-10-05.
The appsec-rules CLI never invokes Semgrep, and export semgrep still emits only the
metadata scaffold.
Project layout
.github/ CI, security, policy-gate, Scorecard, Pages, and release workflows
docs/adr/ Architecture decision records
docs/assets/ CLI demo and social preview
examples/ Starter pack, worked review, and CI integration examples
exports/ Derived artifacts and the optional executable Semgrep rules
rules/ The baseline rules pack
site/ Source of the project landing page
src/appsec_rules_pack/
Validator, review checker, CLI, exporters, schemas, starter pack
tests/ Test suite and pass, fail, and warning fixtures
Documentation
| Document | Contents |
|---|---|
CHANGELOG.md |
Changes per release |
VERSIONING.md |
Version numbers, compatibility rules, public surface, issue codes |
STATUS.md |
Current state, dated verification results, risks and limits |
ROADMAP.md |
What shipped and what comes next |
TECHNICAL_SPEC.md |
Rule contract and validation design |
docs/rule-fields.md |
Every pack and rule field, and how to adapt the baseline |
docs/v1-readiness.md |
Proposed 1.0 contract freeze, migration policy, and pending acceptance criteria |
docs/adr/ |
Architecture decisions |
Contributing, security, and license
Used the pack on your own work? The 1.0 release waits for that kind of evidence: tell us what worked and what did not with the adoption feedback form.
Contributions are welcome. Read CONTRIBUTING.md for rule authoring
principles, the severity and exception models, and the required checks:
python -m ruff check .
python -m pytest --cov=appsec_rules_pack --cov-report=term-missing
python -m appsec_rules_pack validate rules --require-examples --fail-on-warnings
python -m build
Participation follows the Code of Conduct. Report vulnerabilities
privately as described in SECURITY.md; do not open a public issue.
Licensed under the Apache License 2.0.
Metadata
Release files for appsec-rules-pack 0.9.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| appsec_rules_pack-0.9.0.tar.gz | 53.8 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| appsec_rules_pack-0.9.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 104.7 kB
Release files / appsec_rules_pack-0.9.0.tar.gz
| Download URL | appsec_rules_pack-0.9.0.tar.gz |
|---|---|
| Size | 53.8 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
26f8521ae1728f1a919f42aa50c90257d7650b7586f03028a4e8f7a42805ef1e
|
|
BLAKE2b-256 checksum How to use checksums |
e1022a2fe82957457dc323a5b04b79a77a5ba9d67f3138324bb3ef9106a68ade
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 7, 2026.
Transparency logRelease files / appsec_rules_pack-0.9.0-py3-none-any.whl
| Download URL | appsec_rules_pack-0.9.0-py3-none-any.whl |
|---|---|
| Size | 50.9 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
cd9ff4e81c31f095e49c1508a352a3cbe1315ae4328866b5b7632d82db97095c
|
|
BLAKE2b-256 checksum How to use checksums |
d708c378fed7f63beaa34b9c5a7068e191527681b26fb4802425e30a26cf09e3
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 7, 2026.
Transparency log