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: Codeward rules define what to search for, not requirements to enforce. When a rule matches, it creates a finding that triggers the configured action.
Search-Based Paradigm
Think of rules as search queries:
- ✅ "Find vulnerabilities with CRITICAL severity"
- ✅ "Find files that don't contain USER instruction"
- ✅ "Find PRs larger than 30 files"
Not as requirements:
- ❌ "Severity must not be CRITICAL"
- ❌ "File must contain USER instruction"
- ❌ "PR must be smaller than 30 files"
How Rules Work
- Rule evaluates → Check if condition matches
- Match found (
Passed = true) → Create a finding - Finding triggers action →
info,warn,block, orignore - Output generated → Send to configured destination (unless
ignore)
Example:
{
"rules": [
{ "field": "Severity", "type": "eq", "value": "CRITICAL" }
],
"actions": { "new": "block" }
}
This rule:
- Searches for vulnerabilities where Severity equals CRITICAL
- When found → Creates a finding
- Then blocks the PR (because action is "block")
Operator Semantics
All operators report when the condition is true (matches):
| Operator | Reports When | Example |
|---|---|---|
eq | Value equals target | Severity equals CRITICAL |
ne | Value does not equal target | Severity does not equal LOW |
gt | Value is greater than target | changed_files is greater than 30 |
lt | Value is less than target | changed_files is less than 5 |
contains | Value contains substring | title contains WIP |
not_contains | Value does not contain substring | Dockerfile does not contain USER |
regex | Value matches pattern | title matches ^feat: |
not_regex | Value does not match pattern | title does not match ^(feat|fix): |
in | Value is in set | Severity is in CRITICAL,HIGH |
not_in | Value is not in set | Category is not in test,mock |
exists | Key or file exists | scripts.test exists |
not_exists | Key or file does not exist | SECURITY.md does not exist |
Common Patterns
Find problems (what you want to block):
{
"name": "find-critical-vulns",
"rules": [
{ "field": "Severity", "type": "eq", "value": "CRITICAL" }
]
}
Find missing requirements:
{
"name": "find-missing-tests",
"rules": [
{ "key": "scripts.test", "type": "not_exists" }
]
}
Find policy violations:
{
"name": "find-large-prs",
"rules": [
{ "key": "changed_files", "type": "gt", "value": "30" }
]
}
Find unwanted content:
{
"name": "find-wip-prs",
"rules": [
{ "key": "title", "type": "contains", "value": "WIP" }
]
}
Multiple Rules (OR Logic)
By default, multiple rules use OR logic — a finding is created if any rule matches:
{
"operator": "OR",
"rules": [
{ "field": "Severity", "type": "eq", "value": "CRITICAL" },
{ "field": "Severity", "type": "eq", "value": "HIGH" }
]
}
This finds vulnerabilities that are either CRITICAL or HIGH.
Multiple Rules (AND Logic)
Use "operator": "AND" to require all rules to match:
{
"operator": "AND",
"rules": [
{ "field": "Severity", "type": "eq", "value": "CRITICAL" },
{ "field": "Relationship", "type": "eq", "value": "direct" }
]
}
This finds vulnerabilities that are both CRITICAL and direct dependencies.
Conditional Logic (Implies)
The implies operator enables "If X then Y" logic:
{
"operator": "implies",
"rules": [
{ "key": "changed_files", "type": "gt", "value": "10" },
{ "key": "files.filename", "file_pattern": "docs/**", "type": "not_exists" }
]
}
This means: "If more than 10 files changed, then report if docs were not updated."
- First rule is the trigger (condition)
- If trigger matches → evaluate remaining rules
- If trigger doesn't match → policy passes silently
Policy Types Overview
| Type | What It Scans | Common Use Cases |
|---|---|---|
| vulnerability | CVEs in dependencies | Block critical/high severity, track fixes |
| license | License names and categories | Block copyleft, flag unknown licenses |
| package | Dependency additions/removals | Review new packages, track version changes |
| file | File contents and filesystem | Enforce config standards, require files |
| pr | PR metadata and file changes | Limit PR size, enforce naming conventions |
Vulnerability Policies
Detect security vulnerabilities (CVEs) in your dependencies using embedded Trivy scanner.
Allowed Fields
Display Fields (for fields and group_by in outputs):
| Field | Description |
|---|---|
VulnerabilityID | CVE identifier (e.g., CVE-2024-1234) |
PkgID | Package identifier |
PkgName | Package name |
InstalledVersion | Currently installed version |
FixedVersion | Version that fixes the vulnerability |
Status | Fix status (e.g., "fixed", "affected") |
Severity | CRITICAL, HIGH, MEDIUM, LOW, UNKNOWN |
Title | Vulnerability title |
Description | Detailed description |
PublishedDate | When vulnerability was published |
LastModifiedDate | When vulnerability was last modified |
PrimaryURL | Primary reference URL |
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) |
Filterable Fields (for policy rules — all 19 fields):
| Field | Description |
|---|---|
VulnerabilityID | CVE identifier (e.g., CVE-2024-1234) |
PkgID | Package identifier |
PkgName | Package name |
InstalledVersion | Currently installed version |
FixedVersion | Version that fixes the vulnerability |
ClosestFixedVersion | Closest relevant fix version (computed) |
Status | Fix status (e.g., "fixed", "affected") |
Severity | CRITICAL, HIGH, MEDIUM, LOW, UNKNOWN |
Title | Vulnerability title |
Description | Detailed description |
PublishedDate | When vulnerability was published |
LastModifiedDate | When vulnerability was last modified |
SeveritySource | Source of severity rating |
PrimaryURL | Primary reference URL |
CweIDs | CWE identifiers (array — matches if any element matches) |
Relationship | direct or indirect (requires dependency_tree: true) |
Children | Child packages (array, requires dependency_tree: true) |
Parents | Parent packages (array, requires dependency_tree: true) |
Sources | Affected sources (array, 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"]
}]
}]
}
Block critical and high:
{
"rules": [
{ "field": "Severity", "type": "eq", "value": "CRITICAL" },
{ "field": "Severity", "type": "eq", "value": "HIGH" }
]
}
Only direct dependencies (block transitive noise):
{
"global": { "dependency_tree": true },
"vulnerability": [{
"name": "direct-only",
"actions": { "new": "block" },
"operator": "AND",
"rules": [
{ "field": "Severity", "type": "eq", "value": "CRITICAL" },
{ "field": "Relationship", "type": "eq", "value": "direct" }
],
"outputs": [{ "format": "markdown", "destination": "git:pr" }]
}]
}
Exclude a specific CVE:
{
"rules": [
{ "field": "Severity", "type": "eq", "value": "CRITICAL" },
{ "field": "VulnerabilityID", "type": "ne", "value": "CVE-2024-ACCEPTED" }
]
}
Filter by CWE category:
{
"rules": [
{ "field": "CweIDs", "type": "eq", "value": "CWE-89" }
]
}
Filter by vulnerability title:
{
"rules": [
{ "field": "Title", "type": "contains", "value": "Injection" }
]
}
Filter by publication date (2024 vulnerabilities):
{
"rules": [
{ "field": "PublishedDate", "type": "regex", "value": "^2024-" }
]
}
Complex multi-field filter (AND logic):
{
"operator": "AND",
"rules": [
{ "field": "Severity", "type": "eq", "value": "CRITICAL" },
{ "field": "CweIDs", "type": "eq", "value": "CWE-89" },
{ "field": "Title", "type": "regex", "value": "(?i)sql.*injection" },
{ "field": "PublishedDate", "type": "regex", "value": "^2024-" }
]
}
License Policies
Detect license issues for compliance. Trivy categorizes licenses by risk level.
Allowed Fields
Display Fields (for fields and group_by in outputs):
| Field | Description |
|---|---|
Severity | License risk severity |
Category | License category (e.g., Permissive, Copyleft, Forbidden) |
PkgName | Package using this license |
Name | License name (e.g., MIT, GPL-3.0) |
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) |
Filterable Fields (for policy rules — all 8 fields):
| Field | Description |
|---|---|
Severity | License risk severity: CRITICAL, HIGH, MEDIUM, LOW, UNKNOWN |
Category | License category (e.g., Permissive, Copyleft, Forbidden) |
PkgName | Package using this license |
Name | License name (e.g., MIT, GPL-3.0) |
Relationship | direct or indirect (requires dependency_tree: true) |
Children | Child packages (array, requires dependency_tree: true) |
Parents | Parent packages (array, requires dependency_tree: true) |
Sources | Affected sources (array, requires dependency_tree: true) |
Examples
Block copyleft licenses:
{
"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"],
"changes": ["new"]
}]
}]
}
Block GPL by name:
{
"rules": [
{ "field": "Name", "type": "contains", "value": "GPL" }
]
}
Flag unknown licenses:
{
"rules": [
{ "field": "Severity", "type": "eq", "value": "UNKNOWN" }
]
}
Package Policies
Track dependency changes: additions, removals, and version updates.
Allowed Fields
Display Fields (for fields and group_by in outputs):
| Field | Description |
|---|---|
ID | Package identifier |
Name | Package name |
Version | Package version |
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) |
Filterable Fields (for policy rules — all 9 fields):
| Field | Description |
|---|---|
ID | Package identifier |
Name | Package name |
Version | Package version |
Relationship | direct or indirect (requires dependency_tree: true) |
PURL | Package URL format identifier |
UID | Package unique identifier |
Children | Child packages (array, requires dependency_tree: true) |
Parents | Parent packages (array, requires dependency_tree: true) |
Sources | Affected sources (array, requires dependency_tree: true) |
Examples
Track new packages:
{
"package": [{
"name": "new-packages",
"actions": { "new": "warn" },
"rules": [],
"outputs": [{
"format": "markdown",
"destination": "git:pr",
"fields": ["Name", "Version"],
"changes": ["new"]
}]
}]
}
Only direct dependencies:
{
"global": { "dependency_tree": true },
"package": [{
"name": "new-direct",
"actions": { "new": "warn" },
"rules": [
{ "field": "Relationship", "type": "eq", "value": "direct" }
],
"outputs": [{
"format": "markdown",
"destination": "git:pr",
"fields": ["Name", "Version", "Relationship"],
"changes": ["new"]
}]
}]
}
Export full inventory:
{
"package": [{
"name": "inventory",
"actions": { "new": "info", "existing": "info", "changed": "info", "removed": "info" },
"rules": [],
"outputs": [{
"format": "json",
"destination": "file:/results/packages.json"
}]
}]
}
File Policies
Assert rules against file contents or filesystem structure. Great for enforcing standards in Dockerfiles, CI configs, package.json, etc.
Additional Required Fields
File policies require extra fields:
| Field | Required | Description |
|---|---|---|
type | Yes | text, json, yaml, yml, or filesystem |
path | Yes | File path or glob pattern to check |
Validation Types and Rule Keys
| Type | Rule Keys | Available Operators |
|---|---|---|
text | type, value | regex, not_regex, contains, not_contains, hasPrefix, hasSuffix, eq, ne |
json | type, key, value | eq, ne, lt, gt, le, ge, contains, not_contains, hasPrefix, hasSuffix, regex, not_regex, exists, not_exists |
yaml / yml | type, key, value | Same as json |
filesystem | type, path | exists, not_exists |
Array Wildcards & Matching
For JSON and YAML policies, you can inspect arrays using the * wildcard in key paths. When a path returns multiple values (e.g., spec.containers.*.image), you can control how the rule is evaluated using the match parameter.
Core Principle: Rules define problem conditions. The match parameter controls how many "safe" values (that don't match the problem) are needed to avoid reporting a finding.
| Match Value | Description | When Finding is Reported |
|---|---|---|
all (default) | Report if ANY value matches condition | At least one value has the problem |
any | Report only if ALL values match condition | No safe values exist |
none | Same as all | At least one value has the problem |
Note: The
matchparameter can only be used with wildcard paths (containing*). Using it with non-wildcard paths will result in a config validation error.
Examples:
-
Block if any container uses :latest tag (match:
all, default):{
"file": [{
"name": "no-latest-images",
"type": "yaml",
"path": "k8s/*.yaml",
"actions": {"existing": "block"},
"rules": [
{
"key": "spec.containers.*.image",
"type": "contains",
"value": ":latest",
"match": "all"
}
]
}]
}With data
["nginx:1.19", "redis:latest"]: Finding reported (one image contains ":latest") With data["nginx:1.19", "redis:6.2"]: No finding (no images contain ":latest") -
Ensure at least one container is NOT named "init" (match:
any):{
"file": [{
"name": "require-non-init-container",
"type": "yaml",
"path": "k8s/*.yaml",
"rules": [
{
"key": "spec.initContainers.*.name",
"type": "eq",
"value": "init",
"match": "any"
}
]
}]
}With data
["init", "other"]: No finding (one safe value exists) With data["init", "init"]: Finding reported (all match problem condition) -
Verify all container images use trusted registry (match:
all):{
"file": [{
"name": "trusted-registry",
"type": "yaml",
"path": "k8s/*.yaml",
"rules": [
{
"key": "spec.containers.*.image",
"type": "not_contains",
"value": "my-registry.io/",
"match": "all"
}
]
}]
}Reports finding if ANY container image does NOT contain the trusted registry prefix.
Output Fields
File policies use different output fields:
| Field | Description |
|---|---|
Key | The rule key checked |
Type | The rule type used |
Value | The expected value |
Expected | The expected value (alias for Value) |
Reason | Explanation of pass/fail |
Passing | Boolean result |
Passed | Boolean result (alias for Passing) |
Path | File path checked |
FilePath | File path (alias for Path) |
RuleRole | Rule role in implies logic: trigger or condition |
Custom Output Reasons
File and PR policies allow you to customize the failure message using the output_reason field. This replaces the generic technical error with actionable instructions for developers.
Example:
{
"rules": [
{
"key": "scripts.test",
"type": "not_exists",
"output_reason": "All packages must have a test script. Please add 'test': 'vitest' to package.json."
}
]
}
Blocking Behavior
File and PR validation use only the existing action since they don't track changes over time. Config validation will warn if new, removed, or changed actions are specified for these policy types.
Examples
Require SECURITY.md file:
{
"file": [{
"name": "find-missing-security-md",
"type": "filesystem",
"path": ".",
"actions": { "existing": "block" },
"rules": [
{ "type": "not_exists", "path": "SECURITY.md" }
],
"outputs": [{
"format": "markdown",
"destination": "git:pr",
"fields": ["Path", "Reason", "Passing"]
}]
}]
}
Dockerfile must have USER instruction:
{
"file": [{
"name": "find-root-containers",
"type": "text",
"path": "Dockerfile",
"actions": { "existing": "block" },
"rules": [
{ "type": "not_contains", "value": "USER" }
],
"outputs": [{
"format": "markdown",
"destination": "git:pr",
"fields": ["Key", "Reason", "Passing"]
}]
}]
}
package.json must have test script:
{
"file": [{
"name": "find-missing-tests",
"type": "json",
"path": "package.json",
"actions": { "existing": "warn" },
"rules": [
{ "type": "not_exists", "key": "scripts.test" }
],
"outputs": [{
"format": "markdown",
"destination": "git:pr"
}]
}]
}
CI workflow must have minimal permissions:
{
"file": [{
"name": "find-overprivileged-workflows",
"type": "yaml",
"path": ".github/workflows/*.yml",
"actions": { "existing": "block" },
"rules": [
{ "type": "ne", "key": "permissions.contents", "value": "read" }
],
"outputs": [{
"format": "markdown",
"destination": "git:pr"
}]
}]
}
PR Policies
Assert rules against pull request metadata and file changes. Only runs when CODEWARD_MODE=diff.
PR-Level Keys
Keys are case-insensitive (e.g., Title, title, and TITLE are equivalent).
| Key | Description |
|---|---|
number | PR number |
title | PR title |
body | PR description/body text |
state | PR state (open, closed, merged) |
draft | Whether PR is a draft (true/false) |
html_url | PR URL on GitHub |
created_at | PR creation timestamp |
updated_at | PR last update timestamp |
merged_at | PR merge timestamp |
commits | Number of commits |
changed_files | Number of files changed |
lines_added | Total lines added |
lines_removed | Total lines removed |
head.ref | Source branch name (e.g., "feature/my-branch") |
head.sha | Source branch commit SHA |
base.ref | Target branch name (e.g., "main") |
base.sha | Target branch commit SHA |
user.login | PR author username |
user.id | PR author user ID |
user.type | Author type (User, Bot, Organization) |
user.site_admin | Whether user is a GitHub site admin |
user.html_url | PR author profile URL |
labels | Array of PR labels |
labels.*.name | Label name (wildcard) |
labels.*.color | Label color hex code (wildcard) |
labels.*.description | Label description (wildcard) |
labels_count | Number of labels |
assignees | Array of assigned users |
assignees.*.login | Assignee username (wildcard) |
assignees.*.id | Assignee user ID (wildcard) |
assignees.*.type | Assignee type (wildcard) |
assignees_count | Number of assignees |
comments_count | Number of comments |
comments.*.user.login | Comment author username (wildcard) |
comments.*.user.type | Comment author type (wildcard) |
comments.*.body | Comment body text (wildcard) |
requested_reviewers_count | Number of requested reviewers |
requested_reviewers.*.login | Requested reviewer username (wildcard) |
requested_reviewers.*.type | Requested reviewer type (wildcard) |
files.new | List of new files |
files.changed | List of changed files |
files.removed | List of removed files |
files.renamed | List of renamed files |
files.count | Total number of changed files |
Wildcard paths: Use
*to match all elements in arrays. See Match Parameter for Wildcards below.
File-Level Keys
Use files.* prefix to check individual file changes (requires file_pattern or file_status):
| Key | Description |
|---|---|
files.filename | File path |
files.status | File status: added, removed, modified, renamed |
files.additions | Lines added in this file |
files.deletions | Lines deleted |
files.changes | Total changes (additions + deletions) |
files.patch | Unified diff content |
files.previous_filename | Original filename (for renamed files) |
files.sha | File content SHA |
File Filtering
| Field | Description |
|---|---|
file_pattern | Regex pattern to match file paths (glob supported as fallback) |
file_status | Filter by status: new, changed, removed, renamed, or * |
action | Per-rule action: info, warn, block, or ignore |
Note: content_rules is not supported. Use files.* keys instead to inspect file contents (e.g., files.patch).
Match Parameter for Wildcards
When using wildcard paths like labels.*.name or comments.*.user.type, use the match parameter to control how multiple values are evaluated:
| Match Value | Description | When Finding is Reported |
|---|---|---|
all (default) | Report finding if ANY value matches | At least ONE value triggers the rule |
any | Report finding only if ALL values match | ALL values match (no safe values) |
none | Same as all | At least ONE value triggers the rule |
Example — Ensure at least one human comment (not from a bot):
{
"pr": [{
"name": "Require Human Review",
"rules": [
{
"key": "comments.*.user.type",
"type": "eq",
"value": "Bot",
"match": "any",
"action": "warn",
"output_reason": "PR needs at least one comment from a human reviewer"
}
]
}]
}
→ Only reports finding if ALL comments are from bots (no human comments)
Example — Detect any bot comment:
{
"pr": [{
"name": "Bot Comment Detected",
"rules": [
{
"key": "comments.*.user.login",
"type": "regex",
"value": "\\[bot\\]$",
"match": "all",
"action": "info"
}
]
}]
}
→ Reports finding if ANY comment is from a bot
Output Fields
PR policies use these output fields:
| Field | Description |
|---|---|
Key | The rule key checked |
Type | The rule type used |
Value | The expected value |
Reason | Explanation of pass/fail |
Passing | Boolean result |
Path | File path checked |
Filename | Filename (for file-scoped rules) |
RuleRole | Rule role in implies logic: trigger or condition |
Custom Output Reasons
PR rules support the output_reason field to override the auto-generated reason message:
{
"rules": [
{
"key": "changed_files",
"type": "ge",
"value": 10,
"action": "warn",
"output_reason": "This is a large PR with many file changes — consider breaking it up"
}
]
}
Examples
Limit PR size:
{
"pr": [{
"name": "find-large-prs",
"rules": [
{ "type": "ge", "key": "changed_files", "value": "20", "action": "warn" },
{ "type": "ge", "key": "lines_added", "value": "500", "action": "warn" }
],
"outputs": [{
"format": "markdown",
"destination": "git:pr"
}]
}]
}
Block WIP PRs:
{
"pr": [{
"name": "find-wip-prs",
"rules": [
{ "type": "contains", "key": "title", "value": "WIP", "action": "block" },
{ "type": "contains", "key": "title", "value": "work in progress", "action": "block" }
],
"outputs": [{
"format": "markdown",
"destination": "git:pr"
}]
}]
}
No changes to sensitive files:
{
"pr": [{
"name": "find-sensitive-file-changes",
"rules": [
{
"type": "exists",
"key": "files.filename",
"file_pattern": ".*\\.env.*",
"action": "block"
}
],
"outputs": [{
"format": "markdown",
"destination": "git:pr"
}]
}]
}
Check patch content for secrets:
{
"pr": [{
"name": "find-hardcoded-secrets",
"rules": [
{
"type": "contains",
"key": "files.patch",
"value": "password=",
"file_status": "*",
"action": "block"
}
],
"outputs": [{
"format": "markdown",
"destination": "git:pr"
}]
}]
}
Rule Operators Reference
Available for all policy types:
| Operator | Description | Example |
|---|---|---|
eq | Equals | "CRITICAL" |
ne | Not equals | Exclude specific values |
lt | Less than | Version or numeric comparison |
gt | Greater than | Version or numeric comparison |
le | Less than or equal | |
ge | Greater than or equal | |
contains | Substring match | "GPL" in license name |
not_contains | Substring not present | |
hasPrefix | Starts with | "@types/" packages |
hasSuffix | Ends with | .test.js files |
regex | Regular expression | "^CVE-2024-.*" |
not_regex | Regular expression non-match | "^(feat|fix|docs):" title format |
in | In set (comma-separated) | "CRITICAL,HIGH" |
not_in | Not in set (comma-separated) | "test,mock" |
Note: Go's regexp uses RE2 which does not support lookahead/lookbehind. Use
not_regexinstead of negative lookahead patterns like(?!...).
Additional for file/PR validation:
| Operator | Description |
|---|---|
exists | Key or file exists |
not_exists | Key or file does not exist |
Conditional Logic (implies)
The implies operator (available at the policy level) enables "If X then Y" logic.
- The first rule is the "trigger".
- If the trigger passes, subsequent rules are evaluated (actions apply).
- If the trigger fails, the policy passes silently (no action).
Example: "If more than 10 files changed, warn if documentation is not updated":
{
"name": "find-undocumented-large-changes",
"operator": "implies",
"rules": [
{ "key": "changed_files", "type": "gt", "value": 10 },
{ "key": "files.filename", "file_pattern": "docs/**", "type": "not_exists" }
]
}
Version-aware comparisons: lt, gt, le, ge automatically detect semantic versions and compare correctly (e.g., 1.10.0 > 1.9.0).
Best Practices
- Start with warnings: Use
warnfor new policies, promote toblockafter validation - Focus on
new: Only blocknewfindings — let existing issues be tracked separately - Keep policies small: One intent per policy makes debugging easier
- Use meaningful names: Policy names appear in reports
- Add JSON outputs: Export to files for dashboards and automation
- Enable dependency_tree: When filtering by
Relationship,Parents, orChildren