Skip to main content
Version: Latest

Outputs

Each policy can have multiple outputs controlling the format, destination, and presentation of results.

Configuration

outputs:
- format: markdown
destination: git:pr
template: table
fields: [VulnerabilityID, PkgName, Severity, FixedVersion]
changes: [new, changed]
group_by: [Severity]
combined: false
collapse: true
title: Security Issues
comment: Review required before merging
FieldRequiredDescription
formatYesmarkdown, html, json, or sarif
destinationYesWhere to send output
templateNotable, text, or combined (not for JSON)
fieldsNoFields to include (defaults per policy type)
changesNoFilter by change category: new, changed, removed, existing
group_byNoGroup results by fields
combinedNoMerge with other outputs at same destination
collapseNoCollapsible section (markdown/html <details> tags)
titleNoSection title
commentNoText below title
webhookNoCustom webhook config (only for url: destinations)

Destinations

DestinationDescription
git:pr / github:prPost as PR comment
git:issue / github:issueCreate/update GitHub issue
github:code-scanningUpload SARIF to GitHub Code Scanning (Security tab)
file:/pathWrite to file
log:stdout / log:stderrPrint to console
url:https://...HTTP request to webhook

git:pr

Posts or updates a PR comment. Requires CODEWARD_MODE=diff and GitHub env vars.

Behavior: If body is empty, existing comment with matching title is deleted. If non-empty, replaces existing comment.

git:issue

Creates or updates a GitHub issue. Searches all states to avoid duplicates. Closed issues are reopened when content reappears.

file:

destination: "file:/results/vulnerabilities.json"

url:

HTTP request to a webhook endpoint. Content-Type auto-detected.

Retry: 3 attempts, 1s backoff. Retries on network errors and 5xx. No retry on 4xx.

Formats

FormatTemplate Required
markdown / mdYes: table, text, or combined
htmlYes: table, text, or combined
jsonNo (template must be empty)
sarifNo (template must be empty)

Default Fields

When fields is omitted, defaults are applied per policy type:

Policy TypeDefault Fields
vulnerabilityVulnerabilityID, Severity, PkgName, InstalledVersion, FixedVersion
licenseName, Severity, Category, PkgName
packageName, Version, Relationship
fileKey, Value, Reason
prKey, Value, Reason

Combining Outputs

Use combined: true to merge multiple policies into a single output:

vulnerability:
- name: critical
outputs:
- { format: markdown, destination: git:pr, combined: true }
license:
- name: gpl
outputs:
- { format: markdown, destination: git:pr, combined: true }

Produces a single PR comment with both sections.

MixAllowed
Markdown + Markdown
HTML + HTML
JSON + JSON
Markdown + HTML✅ (separate groups)
JSON + Markdown❌ Error

Empty combined groups are suppressed entirely.

SARIF Output

Support for SARIF 2.1.0 (Static Analysis Results Interchange Format), enabling native integration with GitHub Security tab, VS Code Problems panel, and Azure DevOps.

Per-Policy SARIF Output

Write individual policy findings as SARIF:

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

GitHub Code Scanning Upload

Upload SARIF directly to GitHub's Security tab:

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

Findings appear as inline PR annotations and in the Security tab → Code scanning alerts.

Environment Variable Shortcuts

# Write all findings as SARIF to a file
CODEWARD_SARIF_OUTPUT=results.sarif

# Upload all findings to GitHub Code Scanning
CODEWARD_SARIF_UPLOAD=true

SARIF Field Mapping

Scanner conceptSARIF field
VulnerabilityID (e.g. CVE-2024-1234)result.ruleId and rules[].id
Policy action (block/warn/info)result.level (error/warning/note)
Sources[] (lockfile paths)locations[].physicalLocation.artifactLocation.uri
PrimaryURLrules[].helpUri
Severityrules[].properties.severity
Title + fix inforesult.message.text
License PkgName:LicenseNameresult.ruleId (prefixed LICENSE-)
License Category + Severityrules[].properties

A single SARIF file can contain both vulnerability and license findings. When triggered by CODEWARD_SARIF_OUTPUT, all finding types are merged into one run.


SBOM Export

Export the scanner's package inventory as a CycloneDX 1.6 JSON Software Bill of Materials (SBOM).

Config-Driven SBOM

# .codeward.yaml — top-level key
sbom:
destination: "file:sbom.cdx.json"
format: "cyclonedx" # cyclonedx (default)
version: "1.6" # CycloneDX spec version (default)
include_dev: false # Exclude dev dependencies

Environment Variable Shortcut

CODEWARD_SBOM_OUTPUT=sbom.cdx.json

CycloneDX Field Mapping

Scanner dataCycloneDX field
Package.Namecomponent.name
Package.Versioncomponent.version
Package.Identifier.PURLcomponent.purl and bom-ref
Package.Relationship (direct/indirect)component.scope (required/optional)
License.Name (SPDX ID)component.licenses[].license.id
Package.Childrendependencies[].dependsOn
Scan timestampmetadata.timestamp
Repository namemetadata.component.name (type: application)

Features

  • De-duplication: Packages appearing in multiple lockfiles produce a single component
  • PURL-based references: Uses Package URL for component identification
  • License attachment: License scan results are mapped to their parent components
  • Dependency graph: Parent/child relationships exported as CycloneDX dependencies
  • Dev dependency filtering: Optional exclusion of development dependencies
  • No external dependencies: Pure JSON construction, no CycloneDX library needed

Custom Webhooks

The webhook field on url: outputs gives full control over the HTTP request:

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

Webhook Fields

FieldTypeDefaultDescription
methodstringPOSTHTTP method: GET, POST, PUT, PATCH, DELETE
headersmapCustom HTTP headers
bodymapCustom JSON body (supports nesting)

Template Variables

Headers and body values support ${variable} placeholders:

VariableSource
${result}Rendered template output
${title}Output title
${repository}CODEWARD_GITHUB_REPOSITORY
${branch}CODEWARD_GITHUB_BRANCH
${pr_number}CODEWARD_GITHUB_PR_NUMBER
${owner}CODEWARD_GITHUB_OWNER
${mode}CODEWARD_MODE
${timestamp}ISO 8601 UTC

Unknown variables fall back to os.Getenv(), enabling secrets passthrough.

Secrets Passthrough

Never hardcode tokens in config files. Use CODEWARD_WEBHOOK_SECRETS:

# GitHub Actions workflow
- uses: codeward-io/[email protected]
with:
webhook_secrets: |
SLACK_TOKEN=${{ secrets.SLACK_TOKEN }}
JIRA_API_KEY=${{ secrets.JIRA_API_KEY }}

The entrypoint exports each KEY=VALUE pair, then unsets CODEWARD_WEBHOOK_SECRETS.

Integration Examples

Slack:

webhook:
method: POST
headers:
Authorization: "Bearer ${SLACK_TOKEN}"
body:
text: "${result}"
channel: "#security"

JIRA:

webhook:
method: POST
headers:
Authorization: "Basic ${JIRA_API_KEY}"
Content-Type: application/json
body:
fields:
project: { key: SEC }
summary: "Security Alert: ${title}"
description: "${result}"
issuetype: { name: Bug }

PagerDuty:

webhook:
method: POST
headers:
Authorization: "Token token=${PAGERDUTY_TOKEN}"
body:
routing_key: "${PAGERDUTY_ROUTING_KEY}"
event_action: trigger
payload:
summary: "${title}"
source: "${repository}"
severity: critical

Webhook Validation

Config validation checks:

  • webhook only valid on url: destinations
  • method must be GET, POST, PUT, PATCH, or DELETE
  • Unclosed ${ variables are flagged as errors
  • Hardcoded secrets in headers produce warnings

Custom Templates

Load custom output templates from the filesystem:

export CODEWARD_TEMPLATES_PATH=/path/to/templates/

Template files: table.markdown.tmpl, text.markdown.tmpl, combined.markdown.tmpl (and .html.tmpl variants).