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 queries the trivy-db BoltDB database directly with built-in lockfile parsers for 17+ ecosystems.
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 |
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, best V4 > V3 > V2) |
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 | ✅ | ✅ | Category (Permissive, Copyleft, Forbidden, etc.) |
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) |
Examples
license:
- name: no-copyleft
actions: { new: block, existing: warn }
rules:
- { field: Category, type: eq, value: Copyleft }
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 |
max_concurrency | No | Parallel workers for glob patterns |
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