Skip to content

Repository files navigation

Codeward Scanner

Codeward Scanner is a lightweight, self-contained security, governance, and compliance tool for software projects. From a single binary, it scans dependencies for vulnerabilities and licenses, validates configuration files and pull requests, and enforces custom policies. Vulnerability intelligence comes from Codeward Intel — queried live over the API, or from downloadable per-ecosystem snapshots for offline and air-gapped use — with no scanner engine or database to bundle. It ships with built-in parsers for 19 lockfile formats across 12 ecosystems — Node.js, Python, Go, Rust, Ruby, PHP, .NET, Java, Swift, Dart, Elixir and C/C++.

Key Features

  • Vulnerability scanning — CVE detection with severity levels, CVSS scoring, fix availability, and dependency tree tracking
  • License detection — Multi-source license resolution with SPDX classification and category-based policies
  • Policy engine — Flexible rules for vulnerabilities, licenses, packages, files, and pull requests
  • File validation — Validate JSON, YAML, TOML, text, .env, and .properties files against policies
  • PR validation — Enforce PR conventions (title format, branch naming, size limits, required labels)
  • Diff detection — Compare scan results over time, categorize findings as new, removed, existing, or changed
  • GitHub integration — PR comments, issues (with smart reopen/close), and Code Scanning via SARIF upload
  • Custom webhooks — Send findings to Slack, JIRA, PagerDuty, or any HTTP endpoint
  • Multiple output formats — Markdown, HTML, JSON, SARIF 2.1.0 to files, GitHub, or webhooks
  • SBOM export — CycloneDX 1.6 JSON Software Bill of Materials
  • Exit code control — Policies with block action cause non-zero exit for CI/CD gating
  • YAML & JSON config — Write policies in .codeward.yaml, .codeward.yml, or .codeward.json

Installation

GitHub Action

- uses: codeward-io/scan@v0.0.4
  with:
    token: ${{ secrets.GITHUB_TOKEN }}

All inputs have sensible defaults from the GitHub Actions context. Available inputs:

Input Default Description
event ${{ github.event_name }} Event name (e.g. pull_request, push)
repository ${{ github.repository }} Repository (owner/name)
current_branch ${{ github.ref }} Current branch ref
pr_number ${{ github.event.number }} Pull request number
token ${{ github.token }} GitHub token
webhook_secrets Multiline KEY=VALUE pairs for webhook templates

The action automatically checks out the base and branch code, pulls the scanner Docker image, and runs in diff mode for pull requests or main mode for pushes. Policy configuration is discovered from .codeward.json, .codeward.yaml, or .codeward.yml in your repository root.

Docker

docker pull ghcr.io/codeward-io/scan:latest
docker run --rm -v $(pwd):/workspace ghcr.io/codeward-io/scan:latest

Binary Download

Pre-built binaries are available for each release:

OS Architecture Binary
Linux amd64, arm64 codeward-scan-linux-amd64 / codeward-scan-linux-arm64
macOS amd64, arm64 codeward-scan-darwin-amd64 / codeward-scan-darwin-arm64
Windows amd64, arm64 codeward-scan-windows-amd64.exe / codeward-scan-windows-arm64.exe

Download from the Releases page.

Quick Start

  1. Create a .codeward.yaml in your repository root:
vulnerability:
  - name: "block-critical"
    actions:
      new: block
      existing: warn
    rules:
      - field: Severity
        type: eq
        value: CRITICAL
    outputs:
      - destination: "github:pr"
        template: table
        format: markdown
  1. Run the scanner:
scan

If you installed a release binary named codeward-scan, use codeward-scan instead of scan.

The scanner automatically discovers config in this order: .codeward.json, .codeward.yaml, .codeward.yml.

Security Scanning

The scanner uses Codeward Intel for vulnerability intelligence.

  • api mode (default) — Query Intel API in real time
  • download mode — Download per-ecosystem snapshot segments and query locally
  • local mode — Use previously cached snapshots only (air-gapped)
  • disabled mode — Skip Intel vulnerability data (scan continues without vulnerability detection)

Configure mode with --intel-mode or CODEWARD_INTEL_MODE.

Vulnerability Detection

  • CVE-based scanning with severity levels: CRITICAL, HIGH, MEDIUM, LOW, UNKNOWN
  • CVSS scoring: CVSSScore (float, best available V4 > V3 > V2) and CVSSVector for fine-grained risk filtering
  • Fix availability: Shows fixed version when available, including closest fixed version
  • Dependency tree: Tracks vulnerability propagation through dependency chains
  • Multi-source scanning: Scans multiple lockfiles and tracks results per source path

License Detection

Multi-source license resolution fills in missing license data:

Priority Source Description
1 Lockfile Parsed directly from lockfile metadata
2 Local filesystem Scans installed dependency directories for LICENSE files
3 Upstream registries Fetches from npm, PyPI, RubyGems, crates.io, NuGet, Hex.pm, pub.dev, Go proxy
4 GitHub API Last resort — fetches LICENSE file from GitHub repos

Results are cached to disk for 30 days. License findings include SPDX identifiers, a category classification, severity levels, and confidence scores.

License Categories

Every license is classified into exactly one category, which sets its default severity. These are the only values 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

Note: Category values are lowercase and exact, and there is no Copyleft category. A rule such as {"field": "Category", "type": "eq", "value": "Copyleft"} parses cleanly but matches nothing — use restricted or reciprocal instead. By default the GPL and LGPL families are both restricted; reciprocal covers MPL-, EPL- and CDDL-style licenses. (With CODEWARD_INTEL_CLASSIFICATION=true, LGPL moves to reciprocal — see below.)

Where the classification comes from

A license is classified by the scanner's built-in table, by Codeward Intel, or by both. Which one wins depends on CODEWARD_INTEL_CLASSIFICATION (--intel-classification), default false.

Gaps are always filled from Intel, in both modes. A license the built-in table does not recognise takes Intel's classification rather than staying unknown — that costs nothing, because nothing was matching an unclassified license anyway. Where Intel publishes a resolved category it is used directly; otherwise its copyleft strength fills in, with strong mapping to restricted (or forbidden for AGPL-family identifiers, whose obligation extends over a network), weak to reciprocal, and none to notice.

Only overruling is gated by the flag.

CODEWARD_INTEL_CLASSIFICATION Behaviour
false (default) The built-in table wins wherever it has a verdict. Differences are not applied, but they are logged once per scan so you can see what would change.
true Intel's category and severity are authoritative. The built-in table becomes a fallback for licenses Intel has no verdict for.

Turning it on is a behaviour change for existing policies, which is why it is opt-in. Some licenses are classified differently by the two, and a rule filtering on the old category stops matching them:

License Built-in Intel Effect
CC-BY-SA family (23 identifiers) restricted reciprocal the largest group — a restricted rule stops matching them
LGPL family restricted reciprocal
Artistic family notice reciprocal
WTFPL forbidden unencumbered fixes a false positive: WTFPL is public-domain equivalent, and a policy blocking forbidden currently fails builds over it
0BSD unencumbered permissive aligns with MIT-0, which has identical no-attribution semantics

Read the logged differences from a real scan before enabling it, and update any policy that filters on a category in the table above.

Supported Ecosystems

19 lockfile formats across 12 ecosystems, with no external parser dependencies:

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

Air-Gapped Support

Use Intel local snapshot mode for air-gapped environments:

scan --intel-mode local

Or via environment variable:

CODEWARD_INTEL_MODE=local

Use CODEWARD_CACHE_DIR (or CODEWARD_INTEL_CACHE) to control snapshot cache location.

Policy Engine

The policy engine evaluates scan results and file/PR content against user-defined rules.

Policy Types

Type Description Target
vulnerability Rules for security vulnerabilities CVE findings
license Rules for software licenses License detections
package Rules for package dependencies Package metadata
file Rules for file content validation Config files, manifests
pr Rules for pull request validation PR metadata and files

Actions

Action Description Exit Code
info Informational only, no impact 0
warn Warning, logged but not blocking 0
block Blocking, causes non-zero exit 1
ignore Suppress from output 0

Actions are triggered based on change type: new, existing, removed, changed.

Rule Types

Type Description Example
eq Equals {"field": "Severity", "type": "eq", "value": "CRITICAL"}
ne Not equals {"field": "Severity", "type": "ne", "value": "LOW"}
lt, gt, le, ge Numeric comparison {"field": "CVSSScore", "type": "ge", "value": "8.5"}
contains Contains substring {"field": "Title", "type": "contains", "value": "injection"}
not_contains Does not contain {"field": "PkgName", "type": "not_contains", "value": "test"}
hasPrefix Starts with {"field": "VulnerabilityID", "type": "hasPrefix", "value": "CVE-2024"}
hasSuffix Ends with {"field": "PkgName", "type": "hasSuffix", "value": "-dev"}
regex Regex match {"field": "Description", "type": "regex", "value": "remote.*code"}
not_regex Regex non-match {"field": "Title", "type": "not_regex", "value": "^(feat\|fix):"}
in In set {"field": "Severity", "type": "in", "value": "CRITICAL,HIGH"}
not_in Not in set {"field": "Category", "type": "not_in", "value": "test,mock"}
exists Field/path exists {"type": "exists", "key": "engines.node"}
not_exists Field/path doesn't exist {"type": "not_exists", "path": ".env.local"}
last_match Last line matching filter matches value {"type": "last_match", "value": "root", "line_filter": "^USER\\s+"}
not_last_match Last line matching filter does NOT match {"type": "not_last_match", "value": "root", "line_filter": "^USER\\s+"}

Note: Regex uses Go's RE2 engine, which guarantees linear-time matching. See RE2 Regex Reference for supported features and workarounds.

Rule Operators

  • and — All rules must match
  • or — Any rule must match (default)
  • implies — Conditional logic: first rule is the trigger, remaining rules are conditions. If the trigger fails, the policy passes silently.

Ignores

Policy-Level Ignores

Suppress specific items within a policy:

vulnerability:
  - name: "block-critical"
    ignore:
      - name: "ignore-test-packages"
        rules:
          - field: PkgName
            type: contains
            value: test

Expiring Ignores

Time-limited suppressions with an expires date (ISO 8601):

ignore:
  - name: "CVE-2024-1234 waiting vendor patch"
    expires: "2026-06-30"
    author: "security-team"
    description: "Vendor confirmed fix in next release"
    rules:
      - field: VulnerabilityID
        type: eq
        value: CVE-2024-1234

Expired rules are automatically skipped. Rules without expires never expire.

Global Ignores

Source/path-based ignores that apply across all policies:

global:
  ignore:
    - name: "ignore-test-files"
      paths:
        - "**/test/**"
        - "**/*_test.go"

Diff Detection

The scanner compares scan results over time and categorizes each finding:

Category Description
New Items in current scan but not in previous
Removed Items in previous scan but not in current
Existing Items unchanged between scans
Changed Items present in both with different values

This enables targeted actions — for example, block new critical vulnerabilities while only warn on existing ones.

File Validation

Validate configuration files and filesystem state with policy rules.

Supported File Types

Type Description
json JSON file validation
yaml / yml YAML file validation
toml TOML file validation
text Plain text validation
env Environment variable files (.env, .env.example)
properties Java properties files (application.properties, gradle.properties)
filesystem File/directory existence checks
(empty) Auto-detect from file extension

Glob Patterns

File paths support glob patterns for multi-file scanning:

file:
  - name: "all-k8s-manifests"
    path: "k8s/**/*.yaml"
    type: yaml
    actions:
      existing: block
    rules:
      - type: exists
        key: apiVersion

Comma-separated patterns are also supported: **/*.yaml,**/*.yml.

Array Wildcards

Use * to match all elements in arrays and numeric indices for specific elements:

file:
  - name: "no-latest-images"
    path: "k8s/**/*.yaml"
    type: yaml
    actions:
      existing: block
    rules:
      - type: not_contains
        key: "spec.containers.*.image"
        value: ":latest"

The match parameter controls wildcard evaluation:

Match Value Description
"all" (default) Report finding if ANY value matches the condition
"any" Report finding only if ALL values match the condition
"none" Same as "all"

Line-Level Text Scanning

Use scan: "lines" to process each line individually with line numbers in output:

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]

Features:

  • Line numbers in output (LineNumber field)
  • Backslash continuation joining — multi-line constructs (Dockerfile RUN blocks, Makefile recipes) are joined into logical lines before evaluation
  • Matched content showing the actual matched substring
  • Per-line implies — when combined with operator: "implies", trigger/condition logic is applied per line

Conditional Validation

Use operator: "implies" with an exists trigger to validate file content only when the file is present:

file:
  - name: "dockerfile-node-version"
    path: "Dockerfile"
    type: text
    operator: implies
    actions:
      existing: warn
    rules:
      - type: exists
      - type: regex
        value: "FROM node:(18|20|22)"
        output_reason: "Dockerfile should use a supported Node.js version"

Cross-File References

Compare values between files using ref_path and ref_key:

file:
  - name: "version-consistency"
    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:
  - type: ge
    key: "engines.node"
    value: "18"
    output_reason: "Node.js 18+ is required for this project"

PR Validation

Validate pull request metadata and file changes.

PR Metadata Keys

Key Description
title PR title
body PR description
draft Whether PR is a draft
changed_files Number of changed files
total_added / total_removed Total lines added/removed
commits Number of commits
head.ref / base.ref Source/target branch name
user.login PR author username
labels.*.name Label names (wildcard)
assignees.*.login Assignee usernames (wildcard)

Example

pr:
  - name: "PR conventions"
    operator: implies
    rules:
      - key: changed_files
        type: ge
        value: 10
        action: info
        output_reason: "Large PR detected (10+ files changed)"
      - type: exists
        file_pattern: ".*\\.md$"
        file_status: changed
        action: warn
        output_reason: "Large PRs should include documentation updates"
    outputs:
      - destination: "github:pr"
        fields: [RuleRole, Key, Value, Reason]

Output & Reporting

Output Destinations

Destination Description
file:path Write to file
github:pr GitHub PR comment
github:issue GitHub issue
github:code-scanning GitHub Code Scanning (SARIF upload)
log:stdout / log:stderr Log to console
url:endpoint POST to webhook

Output Formats

Format Description
markdown / md Markdown formatting
html HTML formatting
json JSON data export
sarif SARIF 2.1.0

Template Types

Template Description
table Tabular data display
text Prose-style text
combined Multiple sections combined

Custom Templates

Load custom templates from the filesystem:

export CODEWARD_TEMPLATES_PATH=/path/to/templates/

Template files: table.markdown.tmpl, table.html.tmpl, text.markdown.tmpl, text.html.tmpl, combined.markdown.tmpl, combined.html.tmpl.

Custom Webhooks

Send findings to any HTTP endpoint with full control over the request:

vulnerability:
  - name: "critical-vulns"
    actions:
      new: block
    outputs:
      - destination: "url:https://hooks.slack.com/services/xxx"
        template: text
        format: markdown
        webhook:
          method: POST
          headers:
            Content-Type: "application/json"
            Authorization: "Bearer ${SLACK_TOKEN}"
          body:
            text: "${result}"
            channel: "#security-alerts"

Template Variables

Headers and body values support ${variable} placeholders:

Variable Description
${result} Rendered template output
${title} Output title
${repository} Repository name
${branch} Branch name
${pr_number} PR number
${owner} Repository owner
${mode} Scan mode
${timestamp} ISO 8601 UTC timestamp

Unknown variable names fall back to environment variable lookup, enabling secrets passthrough.

Secrets Passthrough

Never hardcode tokens in config files. Use the GitHub Action's webhook_secrets input:

- uses: codeward-io/scan@v0.0.4
  with:
    webhook_secrets: |
      SLACK_TOKEN=${{ secrets.SLACK_TOKEN }}
      JIRA_API_KEY=${{ secrets.JIRA_API_KEY }}

Webhook requests retry up to 3 times with 1-second backoff on 5xx and network errors.

SARIF Output

SARIF 2.1.0 output integrates with GitHub Security tab, VS Code Problems panel, and Azure DevOps.

Per-Policy SARIF

vulnerability:
  - name: "all-vulns"
    outputs:
      - destination: "file:results.sarif"
        format: sarif

GitHub Code Scanning

vulnerability:
  - name: "all-vulns"
    outputs:
      - destination: "github:code-scanning"
        format: sarif

Environment Variable Shortcuts

CODEWARD_SARIF_OUTPUT=results.sarif    # Write all findings as SARIF to a file
CODEWARD_SARIF_UPLOAD=true             # Upload all findings to GitHub Code Scanning

SBOM Export

Export a CycloneDX 1.6 JSON Software Bill of Materials:

sbom:
  destination: "file:sbom.cdx.json"
  format: cyclonedx
  include_dev: false

Or via environment variable:

CODEWARD_SBOM_OUTPUT=sbom.cdx.json

Features: de-duplication across lockfiles, PURL-based component identification, license attachment, dependency graph export, and optional dev dependency filtering.

GitHub Integration

PR Comments

  • Create, update, and delete comments matched by ## Title header
  • Collapsible content support

GitHub Issues

  • Create and update issues matched by title
  • Reopen closed issues when violations recur
  • Close issues when violations are resolved

Code Scanning

  • Upload SARIF findings to the Security tab
  • Inline PR annotations on pull requests
  • Reads GITHUB_SHA and GITHUB_REF automatically in GitHub Actions

Rate Limiting

  • Automatic retry on 429 and 403 rate limit responses
  • Exponential backoff with Retry-After / X-RateLimit-Reset header parsing

Authentication

Set CODEWARD_GITHUB_TOKEN with the required scopes:

  • repo for private repos, public_repo for public
  • security_events (or Code scanning alerts: write for fine-grained tokens) for Code Scanning

CLI Usage

# Show help
scan --help

# Scan with default config
scan

# Scan with custom config
scan --config /path/to/config.yaml

# Scan in diff mode
scan --mode diff

# Run with Intel snapshot download mode
scan --intel-mode download

# Enable debug logging
scan --log-level DEBUG

# Export raw diff data
scan --export-diff=output.json

If you installed a release binary named codeward-scan, replace scan with codeward-scan.

CLI Arguments

Flag Short Description
--config -c Path to config file (.yaml, .yml, or .json)
--private-config Path to private config file
--mode -m Scan mode: main or diff
--intel-mode Intel mode: api, download, local, disabled
--intel-required Fail the scan if no vulnerability data was obtained (default true)
--intel-classification Use Intel's license category/severity instead of the built-in table (default false)
--log-level Log level: DEBUG, INFO, WARN, ERROR
--log-format Log format: text or json
--export-diff Export raw diff results to JSON file
--version -v Show version
--help -h Show help

Configuration Precedence

  1. CLI arguments (highest priority)
  2. Environment variables
  3. Config files
  4. Default values

Configuration

Config File Discovery

The scanner looks for config files in this order (first found wins):

.codeward.json  →  .codeward.yaml  →  .codeward.yml

Use --config / -c to specify a file explicitly. YAML and JSON configs are fully equivalent — every feature works in both formats.

Environment Variables

Variable Default Description
CODEWARD_MODE main Scan mode (diff, main)
CODEWARD_CONFIG_PATH auto-discovery Main config path override
CODEWARD_PRIVATE_CONFIG_PATH mode-dependent default Private config path override
CODEWARD_CACHE_DIR Base cache directory
CODEWARD_TEMPLATES_PATH ./internal/templates/ Custom templates directory
CODEWARD_LOG_LEVEL INFO Log level (DEBUG, INFO, WARN, ERROR)
CODEWARD_LOG_FORMAT text Log format (text, json)
CODEWARD_LOG_OUTPUT stderr Log output (stderr, stdout)
CODEWARD_LOG_TIMESTAMP false Enable log timestamps (true, false)
CODEWARD_LOG_SUMMARY Summary level (none, minimal, standard, detailed)
CODEWARD_SARIF_OUTPUT Write all findings as SARIF to file
CODEWARD_SARIF_UPLOAD false Upload SARIF to GitHub Code Scanning
CODEWARD_SBOM_OUTPUT Write CycloneDX SBOM to file
CODEWARD_GITHUB_TOKEN GitHub API token
CODEWARD_GITHUB_OWNER GitHub repository owner
CODEWARD_GITHUB_REPOSITORY GitHub repository name
CODEWARD_GITHUB_PR_NUMBER PR number for commenting
CODEWARD_GITHUB_BRANCH Branch name
CODEWARD_INTEL_API https://intel.codeward.io Intel API base URL
CODEWARD_INTEL_TOKEN Intel API auth token
CODEWARD_INTEL_MODE api Intel mode (api, download, local, disabled)
CODEWARD_INTEL_CACHE Intel snapshot cache directory override
CODEWARD_INTEL_SEGMENTS Extra snapshot segments (cwes, enrichments, rules, all)
CODEWARD_INTEL_REQUIRED true Fail the scan if no vulnerability data was obtained
CODEWARD_INTEL_CLASSIFICATION false Use Intel's resolved license category/severity instead of the built-in table
CODEWARD_INTEL_PUBKEY Override the built-in key used to verify snapshot manifests (self-hosted Intel only)
CODEWARD_API https://api.codeward.io Codeward API endpoint
CODEWARD_TOKEN Codeward service token
CODEWARD_AI_TOKEN Codeward AI token
CODEWARD_WEBHOOK_SECRETS Webhook secrets (KEY=VALUE per line)
CODEWARD_EXTRA_ENV Extra environment variables (KEY=VALUE per line)

Config Validation

The scanner validates all policies before running:

  • Rule types: Validates against allowed types
  • Output fields: Validates fields and group_by against valid fields per policy type
  • Template requirements: Requires template for markdown/html formats
  • Destination format: Validates output destination prefixes
  • Webhook validation: Checks HTTP methods, unclosed variables, and hardcoded secrets

Invalid policies are skipped with warnings while valid policies continue to run. The scan only fails completely if the config file has invalid syntax or ALL policies have validation errors.

RE2 Regex Reference

The scanner uses Go's RE2 engine, which guarantees linear-time matching. Some PCRE features are unavailable, but the scanner provides built-in workarounds:

Supported:

Feature Syntax Example
Case-insensitive (?i) (?i)password
Multiline (?m) (?m)^FROM
Dot matches newline (?s) (?s)start.*end
Word boundary \b \broot\b
Character classes [...], \d, \w, \s [A-Z]{3,}
Alternation | error|warning|fatal

Unsupported PCRE features and workarounds:

PCRE Feature Workaround
Negative lookahead (?!...) Use not_regex rule type
Positive lookahead (?=...) Split into two regex rules with and operator
Lookbehind (?<=...) Use last_match/not_last_match with line_filter

License

Apache License 2.0 — see LICENSE.

About

Codeward Scanner is a lightweight, self-contained governance, security, and compliance scanning tool for software projects. It draws vulnerability data from Codeward Intel (live API or offline per-ecosystem snapshots) and ships with 19+ built-in lockfile parsers spanning Node.js, Python, Go, Rust, Ruby, PHP, .NET, Java, Swift, and more.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages