Skip to main content
Version: 0.2.0

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

  1. Rule evaluates → Check if condition matches
  2. Match found (Passed = true) → Create a finding
  3. Finding triggers actioninfo, warn, block, or ignore
  4. 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):

OperatorReports WhenExample
eqValue equals targetSeverity equals CRITICAL
neValue does not equal targetSeverity does not equal LOW
gtValue is greater than targetchanged_files is greater than 30
ltValue is less than targetchanged_files is less than 5
containsValue contains substringtitle contains WIP
not_containsValue does not contain substringDockerfile does not contain USER
regexValue matches patterntitle matches ^feat:
not_regexValue does not match patterntitle does not match ^(feat|fix):
inValue is in setSeverity is in CRITICAL,HIGH
not_inValue is not in setCategory is not in test,mock
existsKey or file existsscripts.test exists
not_existsKey or file does not existSECURITY.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

TypeWhat It ScansCommon Use Cases
vulnerabilityCVEs in dependenciesBlock critical/high severity, track fixes
licenseLicense names and categoriesBlock copyleft, flag unknown licenses
packageDependency additions/removalsReview new packages, track version changes
fileFile contents and filesystemEnforce config standards, require files
prPR metadata and file changesLimit 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):

FieldDescription
VulnerabilityIDCVE identifier (e.g., CVE-2024-1234)
PkgIDPackage identifier
PkgNamePackage name
InstalledVersionCurrently installed version
FixedVersionVersion that fixes the vulnerability
StatusFix status (e.g., "fixed", "affected")
SeverityCRITICAL, HIGH, MEDIUM, LOW, UNKNOWN
TitleVulnerability title
DescriptionDetailed description
PublishedDateWhen vulnerability was published
LastModifiedDateWhen vulnerability was last modified
PrimaryURLPrimary reference URL
Relationshipdirect or indirect (requires dependency_tree: true)
ChildrenChild packages (requires dependency_tree: true)
ParentsParent packages (requires dependency_tree: true)
SourcesAffected sources (requires dependency_tree: true)

Filterable Fields (for policy rules — all 19 fields):

FieldDescription
VulnerabilityIDCVE identifier (e.g., CVE-2024-1234)
PkgIDPackage identifier
PkgNamePackage name
InstalledVersionCurrently installed version
FixedVersionVersion that fixes the vulnerability
ClosestFixedVersionClosest relevant fix version (computed)
StatusFix status (e.g., "fixed", "affected")
SeverityCRITICAL, HIGH, MEDIUM, LOW, UNKNOWN
TitleVulnerability title
DescriptionDetailed description
PublishedDateWhen vulnerability was published
LastModifiedDateWhen vulnerability was last modified
SeveritySourceSource of severity rating
PrimaryURLPrimary reference URL
CweIDsCWE identifiers (array — matches if any element matches)
Relationshipdirect or indirect (requires dependency_tree: true)
ChildrenChild packages (array, requires dependency_tree: true)
ParentsParent packages (array, requires dependency_tree: true)
SourcesAffected 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):

FieldDescription
SeverityLicense risk severity
CategoryLicense category (e.g., Permissive, Copyleft, Forbidden)
PkgNamePackage using this license
NameLicense name (e.g., MIT, GPL-3.0)
Relationshipdirect or indirect (requires dependency_tree: true)
ChildrenChild packages (requires dependency_tree: true)
ParentsParent packages (requires dependency_tree: true)
SourcesAffected sources (requires dependency_tree: true)

Filterable Fields (for policy rules — all 8 fields):

FieldDescription
SeverityLicense risk severity: CRITICAL, HIGH, MEDIUM, LOW, UNKNOWN
CategoryLicense category (e.g., Permissive, Copyleft, Forbidden)
PkgNamePackage using this license
NameLicense name (e.g., MIT, GPL-3.0)
Relationshipdirect or indirect (requires dependency_tree: true)
ChildrenChild packages (array, requires dependency_tree: true)
ParentsParent packages (array, requires dependency_tree: true)
SourcesAffected 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):

FieldDescription
IDPackage identifier
NamePackage name
VersionPackage version
Relationshipdirect or indirect (requires dependency_tree: true)
ChildrenChild packages (requires dependency_tree: true)
ParentsParent packages (requires dependency_tree: true)
SourcesAffected sources (requires dependency_tree: true)

Filterable Fields (for policy rules — all 9 fields):

FieldDescription
IDPackage identifier
NamePackage name
VersionPackage version
Relationshipdirect or indirect (requires dependency_tree: true)
PURLPackage URL format identifier
UIDPackage unique identifier
ChildrenChild packages (array, requires dependency_tree: true)
ParentsParent packages (array, requires dependency_tree: true)
SourcesAffected 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:

FieldRequiredDescription
typeYestext, json, yaml, yml, or filesystem
pathYesFile path or glob pattern to check

Validation Types and Rule Keys

TypeRule KeysAvailable Operators
texttype, valueregex, not_regex, contains, not_contains, hasPrefix, hasSuffix, eq, ne
jsontype, key, valueeq, ne, lt, gt, le, ge, contains, not_contains, hasPrefix, hasSuffix, regex, not_regex, exists, not_exists
yaml / ymltype, key, valueSame as json
filesystemtype, pathexists, 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 ValueDescriptionWhen Finding is Reported
all (default)Report if ANY value matches conditionAt least one value has the problem
anyReport only if ALL values match conditionNo safe values exist
noneSame as allAt least one value has the problem

Note: The match parameter can only be used with wildcard paths (containing *). Using it with non-wildcard paths will result in a config validation error.

Examples:

  1. 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")

  2. 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)

  3. 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:

FieldDescription
KeyThe rule key checked
TypeThe rule type used
ValueThe expected value
ExpectedThe expected value (alias for Value)
ReasonExplanation of pass/fail
PassingBoolean result
PassedBoolean result (alias for Passing)
PathFile path checked
FilePathFile path (alias for Path)
RuleRoleRule 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).

KeyDescription
numberPR number
titlePR title
bodyPR description/body text
statePR state (open, closed, merged)
draftWhether PR is a draft (true/false)
html_urlPR URL on GitHub
created_atPR creation timestamp
updated_atPR last update timestamp
merged_atPR merge timestamp
commitsNumber of commits
changed_filesNumber of files changed
lines_addedTotal lines added
lines_removedTotal lines removed
head.refSource branch name (e.g., "feature/my-branch")
head.shaSource branch commit SHA
base.refTarget branch name (e.g., "main")
base.shaTarget branch commit SHA
user.loginPR author username
user.idPR author user ID
user.typeAuthor type (User, Bot, Organization)
user.site_adminWhether user is a GitHub site admin
user.html_urlPR author profile URL
labelsArray of PR labels
labels.*.nameLabel name (wildcard)
labels.*.colorLabel color hex code (wildcard)
labels.*.descriptionLabel description (wildcard)
labels_countNumber of labels
assigneesArray of assigned users
assignees.*.loginAssignee username (wildcard)
assignees.*.idAssignee user ID (wildcard)
assignees.*.typeAssignee type (wildcard)
assignees_countNumber of assignees
comments_countNumber of comments
comments.*.user.loginComment author username (wildcard)
comments.*.user.typeComment author type (wildcard)
comments.*.bodyComment body text (wildcard)
requested_reviewers_countNumber of requested reviewers
requested_reviewers.*.loginRequested reviewer username (wildcard)
requested_reviewers.*.typeRequested reviewer type (wildcard)
files.newList of new files
files.changedList of changed files
files.removedList of removed files
files.renamedList of renamed files
files.countTotal 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):

KeyDescription
files.filenameFile path
files.statusFile status: added, removed, modified, renamed
files.additionsLines added in this file
files.deletionsLines deleted
files.changesTotal changes (additions + deletions)
files.patchUnified diff content
files.previous_filenameOriginal filename (for renamed files)
files.shaFile content SHA

File Filtering

FieldDescription
file_patternRegex pattern to match file paths (glob supported as fallback)
file_statusFilter by status: new, changed, removed, renamed, or *
actionPer-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 ValueDescriptionWhen Finding is Reported
all (default)Report finding if ANY value matchesAt least ONE value triggers the rule
anyReport finding only if ALL values matchALL values match (no safe values)
noneSame as allAt 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:

FieldDescription
KeyThe rule key checked
TypeThe rule type used
ValueThe expected value
ReasonExplanation of pass/fail
PassingBoolean result
PathFile path checked
FilenameFilename (for file-scoped rules)
RuleRoleRule 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:

OperatorDescriptionExample
eqEquals"CRITICAL"
neNot equalsExclude specific values
ltLess thanVersion or numeric comparison
gtGreater thanVersion or numeric comparison
leLess than or equal
geGreater than or equal
containsSubstring match"GPL" in license name
not_containsSubstring not present
hasPrefixStarts with"@types/" packages
hasSuffixEnds with.test.js files
regexRegular expression"^CVE-2024-.*"
not_regexRegular expression non-match"^(feat|fix|docs):" title format
inIn set (comma-separated)"CRITICAL,HIGH"
not_inNot in set (comma-separated)"test,mock"

Note: Go's regexp uses RE2 which does not support lookahead/lookbehind. Use not_regex instead of negative lookahead patterns like (?!...).

Additional for file/PR validation:

OperatorDescription
existsKey or file exists
not_existsKey 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

  1. Start with warnings: Use warn for new policies, promote to block after validation
  2. Focus on new: Only block new findings — let existing issues be tracked separately
  3. Keep policies small: One intent per policy makes debugging easier
  4. Use meaningful names: Policy names appear in reports
  5. Add JSON outputs: Export to files for dashboards and automation
  6. Enable dependency_tree: When filtering by Relationship, Parents, or Children