Policies
Codeward supports five policy types. Each scans for different concerns but shares the same configuration pattern for actions, rules, and outputs.
Understanding Rule Logic
Critical concept: Rules define what to search for, not requirements to enforce. When a rule matches, it creates a finding that triggers the configured action.
Think of rules as search queries:
- ✅ "Find vulnerabilities with CRITICAL severity" → block
- ✅ "Find files that don't contain USER instruction" → warn
- ✅ "Find PRs larger than 30 files" → warn
How It Works
- Rule evaluates → checks if condition matches
- Match found → creates a finding
- Finding triggers action →
info,warn,block, orignore - Output generated → sent to configured destination
Vulnerability Policies
Detect CVEs in dependencies. The scanner parses lockfiles with its own built-in parsers — 19 formats across 12 ecosystems — and resolves them against Codeward Intel, which supplies the advisory data, severity, fix versions and enrichment (EPSS, CISA KEV, exploit maturity, CVSS v4).
Supported Ecosystems
| Ecosystem | Lockfiles |
|---|---|
| Node.js | package-lock.json, yarn.lock, pnpm-lock.yaml, bun.lock |
| Python | Pipfile.lock, poetry.lock, requirements.txt, uv.lock |
| Ruby | Gemfile.lock |
| Go | go.mod |
| Rust | Cargo.lock |
| PHP | composer.lock |
| .NET | packages.lock.json |
| Dart/Flutter | pubspec.lock |
| Swift | Package.resolved, Podfile.lock |
| Java | gradle.lockfile |
| Elixir | mix.lock |
| C/C++ | conan.lock |
Signed snapshots — the default, keyless data path — carry vulnerabilities for Node.js, Python,
Ruby, Go, Rust, PHP, .NET and Java only. Dart/Flutter, Swift, CocoaPods, Elixir and C/C++
lockfiles are still parsed, and still produce package and licence findings, but yield no
vulnerability findings offline and no warning that they are missing. Use api mode with a key if
you depend on those ecosystems — see
Intel.
Fields
| Field | Filterable | Displayable | Description |
|---|---|---|---|
VulnerabilityID | ✅ | ✅ | CVE identifier |
PkgName | ✅ | ✅ | Package name |
PkgID | ✅ | ✅ | Package identifier |
InstalledVersion | ✅ | ✅ | Installed version |
FixedVersion | ✅ | ✅ | Version with fix |
ClosestFixedVersion | ✅ | Closest relevant fix version | |
Severity | ✅ | ✅ | CRITICAL, HIGH, MEDIUM, LOW, UNKNOWN |
Status | ✅ | ✅ | Fix status |
Title | ✅ | ✅ | Vulnerability title |
Description | ✅ | ✅ | Detailed description |
PublishedDate | ✅ | ✅ | Publication date |
LastModifiedDate | ✅ | ✅ | Last modified date |
PrimaryURL | ✅ | ✅ | Reference URL |
SeveritySource | ✅ | Source of severity rating | |
CweIDs | ✅ | CWE identifiers (matches if any element matches) | |
CVSSScore | ✅ | ✅ | CVSS score (float). Prefers V4 > V3 > V2, but Intel publishes no V4 today, so in practice this is the V3 base score |
CVSSVector | ✅ | ✅ | Full CVSS vector string |
Relationship | ✅ | ✅ | direct or indirect (requires dependency_tree: true) |
Children | ✅ | ✅ | Child packages (requires dependency_tree: true) |
Parents | ✅ | ✅ | Parent packages (requires dependency_tree: true) |
Sources | ✅ | ✅ | Affected sources (requires dependency_tree: true) |
Examples
# Block new critical vulnerabilities
vulnerability:
- name: block-critical
actions: { new: block, existing: warn }
rules:
- { field: Severity, type: eq, value: CRITICAL }
outputs:
- format: markdown
destination: git:pr
fields: [VulnerabilityID, PkgName, Severity, FixedVersion]
changes: [new]
# Filter by CVSS score
- name: high-cvss
actions: { new: block }
rules:
- { field: CVSSScore, type: gt, value: "9.0" }
# Only direct dependencies
- name: direct-only
actions: { new: block }
operator: and
rules:
- { field: Severity, type: eq, value: CRITICAL }
- { field: Relationship, type: eq, value: direct }
License Policies
Detect license compliance issues. Multi-source resolution fills in missing license data from lockfiles, local filesystem, upstream registries (npm, PyPI, RubyGems, crates.io, NuGet, Hex.pm, pub.dev, Go proxy), and GitHub API.
License Resolution Priority
| Priority | Source | Description |
|---|---|---|
| 1 | Lockfile | Parsed from lockfile metadata |
| 2 | Local filesystem | LICENSE files in dependency directories |
| 3 | Upstream registries | Registry APIs (npm, PyPI, etc.) |
| 4 | GitHub API | Last resort for Go modules |
Results are cached to disk for 30 days.
Fields
| Field | Filterable | Displayable | Description |
|---|---|---|---|
Name | ✅ | ✅ | License name (e.g., MIT, GPL-3.0) |
Severity | ✅ | ✅ | Risk severity |
Category | ✅ | ✅ | License category — see License categories |
PkgName | ✅ | ✅ | Package using this license |
LicenseSource | ✅ | ✅ | Where license was resolved from |
Relationship | ✅ | ✅ | direct or indirect (requires dependency_tree: true) |
Children | ✅ | ✅ | Child packages (requires dependency_tree: true) |
Parents | ✅ | ✅ | Parent packages (requires dependency_tree: true) |
Sources | ✅ | ✅ | Affected sources (requires dependency_tree: true) |
License categories
Every license is classified into exactly one category, which also sets its default severity. These lowercase values are the only ones a Category rule can match:
| Category | Severity | Examples |
|---|---|---|
forbidden | CRITICAL | AGPL-1.0, AGPL-3.0 — network copyleft |
restricted | HIGH | GPL-2.0, GPL-3.0, LGPL-3.0 |
reciprocal | MEDIUM | MPL-2.0, EPL-2.0, CDDL-1.0 |
notice | LOW | MIT, Apache-2.0, BSD-3-Clause, Zlib |
permissive | LOW | MIT-0, BlueOak-1.0.0, Unicode-3.0 |
unencumbered | LOW | CC0-1.0, Unlicense — public domain |
unknown | UNKNOWN | Unrecognised or non-SPDX license text |
Copyleft categoryCategory values are lowercase and matched exactly. A rule such as
{ field: Category, type: eq, value: Copyleft } parses cleanly but matches
nothing. Use restricted for the GPL and LGPL families, or reciprocal for
MPL-, EPL- and CDDL-style licenses.
Where categories come from
The category vocabulary is Codeward Intel's, not the scanner's. Intel derives it from the ScanCode LicenseDB category corpus plus choosealicense.com obligation data, so classifications improve without a scanner release. The scanner ships a built-in table as a fallback for offline scans.
Which one decides is controlled by CODEWARD_INTEL_CLASSIFICATION
(--intel-classification), default false:
- Gaps are filled from Intel either way. A license the built-in table does
not recognise takes Intel's classification instead of staying
unknown. - Only overruling is gated. With the flag
false, the built-in table wins wherever it has a verdict, and any difference is logged once per scan. With ittrue, Intel's category and severity are authoritative.
Some licenses are classified differently by the two, so a rule filtering on the
old category stops matching them. The largest group is the CC-BY-SA family
(23 identifiers), which moves restricted → reciprocal; the LGPL family
and the Artistic family also become reciprocal. Two are corrections:
WTFPL moves forbidden → unencumbered (it is public-domain equivalent, so
a policy blocking forbidden currently fails builds over it) and 0BSD moves
unencumbered → permissive (matching MIT-0, which has identical semantics).
Run a scan with the flag off first and read the logged differences before turning it on.
Examples
license:
- name: no-copyleft
actions: { new: block, existing: warn }
rules:
- { field: Category, type: in, value: "restricted,forbidden" }
outputs:
- format: markdown
destination: git:pr
fields: [Name, Category, PkgName, LicenseSource]
changes: [new]
Package Policies
Track dependency changes: additions, removals, and version updates.
Fields
| Field | Filterable | Displayable | Description |
|---|---|---|---|
ID | ✅ | ✅ | Package identifier |
Name | ✅ | ✅ | Package name |
Version | ✅ | ✅ | Package version |
PURL | ✅ | Package URL | |
UID | ✅ | Unique identifier | |
Relationship | ✅ | ✅ | direct or indirect (requires dependency_tree: true) |
Children | ✅ | ✅ | Child packages (requires dependency_tree: true) |
Parents | ✅ | ✅ | Parent packages (requires dependency_tree: true) |
Sources | ✅ | ✅ | Affected sources (requires dependency_tree: true) |
Examples
package:
- name: new-packages
actions: { new: warn }
rules: []
outputs:
- format: markdown
destination: git:pr
fields: [Name, Version]
changes: [new]
File Policies
Validate file contents and filesystem state. Supports JSON, YAML, TOML, text, env, properties, and filesystem checks.
Policy-Specific Fields
| Field | Required | Description |
|---|---|---|
type | No | json, yaml/yml, toml, text, env, properties, filesystem, or empty (auto-detect) |
path | Yes | File path or glob pattern |
scan | No | Set to lines for line-level text scanning |
Glob patterns are expanded and validated in parallel, with one worker per CPU. That is not configurable.
File Types
| Type | Description | Auto-detected by |
|---|---|---|
json | JSON file validation | .json extension |
yaml / yml | YAML file validation | .yaml, .yml extension |
toml | TOML file validation | .toml extension |
text | Plain text validation | Fallback |
env | KEY=VALUE format (.env files) | .env extension |
properties | Java properties format | .properties extension |
filesystem | File/directory existence | — |
| (empty) | Auto-detect from extension | Extension-based |
Glob Patterns
File paths support glob patterns for multi-file scanning:
| Pattern | Description |
|---|---|
**/*.yaml | All YAML files recursively |
**/*.yaml,**/*.yml | Comma-separated patterns |
src/**/*.json | JSON files in src tree |
config[0-9].json | Character class matching |
Comma-separated patterns: spaces are trimmed, trailing commas ignored, results deduplicated.
Array Wildcards & Match Parameter
Use * in key paths to match array elements (e.g., spec.containers.*.image). Control evaluation with match:
| Match | When Finding is Reported |
|---|---|
all (default) | ANY value matches the condition |
any | ALL values match (no safe values exist) |
none | ANY value matches (same as all) |
file:
# Block if any container uses :latest
- name: no-latest
path: "k8s/**/*.yaml"
type: yaml
actions: { existing: block }
rules:
- key: spec.containers.*.image
type: contains
value: ":latest"
match: all
# Require at least one non-init container
- name: require-non-init
path: "k8s/**/*.yaml"
type: yaml
rules:
- key: spec.initContainers.*.name
type: eq
value: init
match: any # reports only if ALL are "init"
match can only be used with wildcard paths containing *.
Line-Level Text Scanning
Use scan: "lines" to process each logical line individually:
- Line numbers in output (
LineNumberfield —"42"for single,"14-17"for continuation blocks) - Backslash continuation joining — lines ending with
\are joined into logical lines - Matched content (
MatchedContentfield, truncated to 120 chars)
file:
- name: detect-aws-keys
path: "**/*.py"
type: text
scan: lines
actions: { existing: block }
rules:
- type: regex
value: "AKIA[0-9A-Z]{16}"
output_reason: "AWS access key detected"
outputs:
- destination: github:pr
fields: [FilePath, LineNumber, MatchedContent, Reason]
Per-Line Implies
Combine operator: "implies" with scan: "lines" for per-line conditional logic:
file:
- name: dockerfile-apt-cleanup
path: Dockerfile
type: text
scan: lines
operator: implies
actions: { existing: block }
rules:
- type: regex
value: "apt-get install" # trigger
- type: regex
value: "rm -rf /var/lib/apt/lists" # condition
output_reason: "apt-get install without cleanup in same RUN block"
Each logical line is evaluated independently: if the trigger matches, conditions are checked on that same line. Lines where the trigger doesn't match are skipped.
Last Match Rules
Evaluate the last logical line matching a line_filter regex. Useful for "the last USER in a Dockerfile must not be root":
| Rule Type | Reports When |
|---|---|
last_match | Last filtered line matches value |
not_last_match | Last filtered line does NOT match value |
file:
- name: dockerfile-non-root
path: Dockerfile
type: text
actions: { existing: block }
rules:
- type: last_match
value: root
line_filter: "^USER\\s+"
output_reason: "Last USER instruction runs as root"
If no lines match line_filter, the rule is silently skipped.
Conditional File Validation (implies + exists)
Check file content only when the file exists using operator: "implies" with an exists trigger:
file:
- name: check-engines-if-exists
path: package.json
type: json
operator: implies
actions: { existing: warn }
rules:
- type: exists # trigger
- key: engines.node
type: exists
output_reason: "package.json should specify engines.node"
If the file doesn't exist, the policy passes silently. Works with all file types.
Cross-File Value References
Compare values between files using ref_path and ref_key:
| Field | Description |
|---|---|
ref_path | Path to reference file (defaults to same file) |
ref_key | Key in reference file (defaults to same key) |
ref_type | Type of reference file: json, yaml, toml, env, properties (auto-detected) |
ref_path/ref_key and value are mutually exclusive.
file:
# Same-file key comparison
- name: port-consistency
path: config.json
type: json
actions: { existing: warn }
rules:
- type: eq
key: server.port
ref_key: proxy.target_port
output_reason: "Server port and proxy target port must match"
# Cross-file comparison
- name: version-sync
path: package.json
type: json
actions: { existing: block }
rules:
- type: eq
key: version
ref_path: Chart.yaml
ref_type: yaml
output_reason: "package.json version must match Chart.yaml version"
Custom Output Reason
Override auto-generated messages with output_reason:
rules:
- key: scripts.test
type: not_exists
output_reason: "All packages must have a test script. Add 'test': 'vitest' to package.json."
Output Fields
| Field | Description |
|---|---|
Key | Rule key checked |
Type | Rule type used |
Value / Expected | Expected value |
Reason | Explanation (or custom output_reason) |
Passed / Passing | Boolean result |
Path / FilePath | File path checked |
RuleRole | trigger or condition (implies logic) |
MatchedContent | Matched substring (line scanning) |
LineNumber | Line number(s) (line scanning) |
Examples
file:
# Require SECURITY.md
- name: require-security-md
type: filesystem
path: "."
actions: { existing: block }
rules:
- { type: not_exists, path: SECURITY.md }
# Dockerfile must have USER
- name: dockerfile-user
type: text
path: Dockerfile
actions: { existing: block }
rules:
- { type: not_contains, value: USER }
# package.json must have test script
- name: require-test-script
type: json
path: package.json
actions: { existing: warn }
rules:
- { type: not_exists, key: scripts.test }
# TOML validation
- name: cargo-edition
type: toml
path: Cargo.toml
actions: { existing: warn }
rules:
- { type: eq, key: package.edition, value: "2021" }
# Env file validation
- name: env-check
type: env
path: .env.example
actions: { existing: warn }
rules:
- { type: exists, key: DATABASE_URL }
PR Policies
Validate pull request metadata and file changes. Only runs when CODEWARD_MODE=diff.
PR Keys
Keys are case-insensitive. All normalized to lowercase internally.
| Key | Description |
|---|---|
number | PR number |
title | PR title |
body | PR description |
state | PR state (open, closed, merged) |
draft | Whether PR is a draft |
changed_files | Number of changed files |
lines_added / lines_removed | Total lines added/removed |
commits | Number of commits |
head.ref / head.sha | Source branch name / SHA |
base.ref / base.sha | Target branch name / SHA |
user.login / user.id / user.type | PR author info |
labels.*.name / labels_count | Label names (wildcard) / count |
assignees.*.login / assignees_count | Assignee names (wildcard) / count |
comments.*.user.login / comments.*.body | Comment info (wildcard) |
comments_count | Number of comments |
requested_reviewers.*.login / requested_reviewers_count | Reviewer info (wildcard) |
files.new / files.changed / files.removed / files.renamed / files.count | File lists and count |
File-Scoped Keys
Use with file_pattern or file_status:
| Key | Description |
|---|---|
files.filename | File path |
files.status | added, removed, modified, renamed |
files.additions / files.deletions / files.changes | Line counts |
files.patch | Unified diff content |
files.previous_filename | Original filename (renamed files) |
Per-Rule Actions
PR rules support per-rule action overrides and output_reason:
pr:
- name: pr-quality
rules:
- key: changed_files
type: ge
value: "20"
action: warn
output_reason: "Large PR — consider breaking it up"
- key: title
type: contains
value: WIP
action: block
Wildcard Match Parameter
Same match parameter as file policies for wildcard paths like labels.*.name:
pr:
- name: require-human-review
rules:
- key: comments.*.user.type
type: eq
value: Bot
match: any # reports only if ALL comments are from bots
action: warn
output_reason: "PR needs at least one human comment"
Output Fields
| Field | Description |
|---|---|
Key | Rule key checked |
Type | Rule type used |
Value / Expected | Expected value |
Reason | Explanation |
Passed / Passing | Boolean result |
Path / FilePath | File path |
Filename | Filename (file-scoped rules) |
RuleRole | trigger or condition |
Examples
pr:
# Limit PR size
- name: pr-size
rules:
- { key: changed_files, type: ge, value: "20", action: warn }
- { key: lines_added, type: ge, value: "500", action: warn }
outputs:
- { format: markdown, destination: git:pr }
# Block WIP PRs
- name: no-wip
rules:
- { key: title, type: contains, value: WIP, action: block }
outputs:
- { format: markdown, destination: git:pr }
# Conditional: if large PR, require docs
- name: docs-for-large-prs
operator: implies
rules:
- { key: changed_files, type: gt, value: "10" }
- { type: exists, file_pattern: "docs/**", file_status: changed, action: warn,
output_reason: "Large PRs should include documentation updates" }
outputs:
- destination: git:pr
fields: [RuleRole, Key, Value, Reason]
RE2 Regex Reference
Go's regexp uses the RE2 engine (linear-time, no backtracking).
Supported:
(?i) case-insensitive, (?m) multiline, (?s) dot-all, \b word boundary, (?P<name>...) named groups, [...] character classes, \d \w \s, alternation |.
Unsupported and workarounds:
| PCRE Feature | Workaround |
|---|---|
Negative lookahead (?!...) | Use not_regex rule type |
Positive lookahead (?=...) | Two regex rules with and operator |
Lookbehind (?<=...) / (?<!...) | Use not_regex or last_match with line_filter |
Backreferences \1 | No workaround |
Best Practices
- Start with warnings — use
warnfirst, promote toblockafter validation - Focus on
new— only block new findings; track existing issues separately - Keep policies small — one intent per policy for easier debugging
- Use meaningful names — policy names appear in reports
- Enable dependency_tree only when needed — for
Relationship,Parents,Childrenfilters