Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 10 additions & 12 deletions .github/workflows/selftest.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,13 @@ permissions:
security-events: write # SARIF → Code Scanning (upload-sarif default true)
pull-requests: write # sticky PR comment on the pull_request job

# TEMPORARY PIN — remove once an engine release with `mcp`-category support is the
# `latest` release. The released engine v0.1.2 predates the `mcp` rule category
# that trustabl-rules now ships, so `latest` engine + latest rules fails to load
# ("unknown category mcp"). Every selftest job is pinned to v0.1.2 + the `pre-mcp`
# rules tag (a known-compatible pair) so CI exercises the ACTION, not the upstream
# engine↔rules drift. Engine fix: trustabl/trustabl#12.
# Engine pin — every selftest job pins `version: v0.1.4` (with the engine's default
# rules) rather than `latest`, so CI is deterministic and exercises the ACTION
# against a known engine. v0.1.4 is the release that introduced the finding
# line-range shape (start_line/end_line) this action consumes, so it validates that
# path end to end. It also supports the `mcp` rule category natively and loads any
# newer rule leniently, so default rules need no compatibility pin. Bump this when a
# newer engine release should gate the action.

jobs:
scan-remote-target:
Expand All @@ -31,8 +32,7 @@ jobs:
continue-on-error: true
with:
target: "https://github.com/openai/openai-agents-python"
version: "v0.1.2" # pinned — see note above
rules-ref: "pre-mcp" # pinned — see note above
version: "v0.1.4" # pinned — see note above
severity-threshold: "none"
risk-score-threshold: "0"
comment-on-pr: "false" # remote target; nothing to comment about on this PR
Expand All @@ -58,8 +58,7 @@ jobs:
- uses: ./
with:
target: "."
version: "v0.1.2"
rules-ref: "pre-mcp"
version: "v0.1.4"
comment-on-pr: "false"

scan-pr-surfaces:
Expand All @@ -72,7 +71,6 @@ jobs:
- uses: ./
with:
target: "."
version: "v0.1.2"
rules-ref: "pre-mcp"
version: "v0.1.4"
severity-threshold: "none" # plumbing check only; don't gate the selftest PR
risk-score-threshold: "0"
35 changes: 35 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,41 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
versions follow [Semantic Versioning](https://semver.org/spec/v2.0.0.html).


## [0.3.0] — 2026-06-09

Tracks trustabl engine **v0.1.4**: consumes the new finding line-range shape and
adds an opt-in dependency CVE scan.

### Added

- **`vuln-scan` input** (default `false`). Passes `--vuln-scan`, so trustabl matches
declared dependencies against a pinned OSV snapshot and reports known CVEs. Each
match is a finding, so it flows through the readiness score, gating, inline
annotations, and the Security tab like any other — plus a dependency headline
(dependencies scanned / known vulnerabilities) in the console panel, Step
Summary, and PR comment.

### Changed

- **Finding line ranges.** The action reads the engine's `start_line`/`end_line`
(engine ≥ v0.1.4) and renders multi-line inline annotations across the finding's
full span. The legacy single `line` field is still read as a fallback, so the
action stays correct against older pinned engines.
- **`skill` scope** added to the typed scope / surface-kind unions, matching the
engine's five detection scopes (tool, agent, subagent, skill, repo).
- **`MIN_ENGINE_VERSION` is now `v0.1.3`** (previously an unset placeholder) — the
release that introduced single-scan dual output, Code-Scanning-valid SARIF, and
the projected-scores headroom ladder. Older engines still run via the two-scan
fallback with a soft upgrade warning.

### Fixed

- **Inline annotations no longer collapse to the top of the file** against engine
v0.1.4. The engine renamed the finding `line` field to `start_line`/`end_line`;
the action still read `line`, so each annotation lost its line number. It now
resolves the range from either shape.


## [0.2.0] — 2026-06-04

A full rewrite from the bash composite action to a **node20 TypeScript action**,
Expand Down
13 changes: 11 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
<p align="center">
<img src="assets/github_banner.jpg" alt="Trustabl — open-source tooling for production-ready agentic tools" width="100%">
</p>

# Trustabl Action

A GitHub Action that runs [trustabl](https://github.com/trustabl/trustabl) — the
Expand All @@ -12,6 +16,9 @@ Agents SDK, Google ADK, MCP) — and surfaces the results where you work:
- **Status-check gating.** Optionally fail the job on a risk-score or severity
threshold so it can be a required check.
- **A readiness panel** in the run log and the Step Summary.
- **Optional dependency CVE scan** (`vuln-scan: true`) — matches your declared
dependencies against a pinned OSV snapshot and reports known CVEs as findings,
so they appear on every surface (score, gate, annotations, Security tab).

It downloads the official `trustabl` release binary (sha256-verified against the
release `checksums.txt`), tool-caches it, scans your checkout, and reports.
Expand Down Expand Up @@ -70,6 +77,7 @@ jobs:
with: # every input is optional
# detectors: openai_sdk # limit SDKs: claude_sdk,openai_sdk,google_adk,openshell
# version: latest # trustabl release to run; pin e.g. v0.5.0 for reproducible CI
# vuln-scan: true # also scan dependencies for known CVEs (OSV)
# severity-threshold: high # fail if any finding >= level (none|low|medium|high|critical)
# risk-score-threshold: 70 # fail if risk (100 - readiness) >= N (0 disables)
# comment-on-pr: true # sticky PR summary comment
Expand All @@ -81,7 +89,7 @@ jobs:
## Pinned + gated

```yaml
- uses: trustabl/trustabl-action@v0.2.0
- uses: trustabl/trustabl-action@v0.3.0
with:
version: v0.5.0
detectors: claude_sdk,openai_sdk
Expand All @@ -98,6 +106,7 @@ jobs:
| `version` | `latest` | trustabl release tag (e.g. `v0.5.0`) or `latest`. |
| `detectors` | _(all)_ | Comma-separated subset: `claude_sdk,openai_sdk,google_adk,openshell`. |
| `strict` | `false` | Pass `--strict` (fail on any finding). |
| `vuln-scan` | `false` | Match dependencies against a pinned OSV snapshot; report known CVEs as findings. |
| `rules-ref` | _(default)_ | Pin a `trustabl-rules` git ref. |
| `rules-repo` | _(default)_ | Override the `trustabl-rules` source repo. |
| `upload-sarif` | `true` | Upload SARIF to Code Scanning. Needs `security-events: write`. |
Expand Down Expand Up @@ -156,7 +165,7 @@ After a run, open the run page and find the **`trustabl-scan-results`** artifact

## Versioning

- Pin a release: `uses: trustabl/trustabl-action@v0.2.0`.
- Pin a release: `uses: trustabl/trustabl-action@v0.3.0`.
- Or track the line: `uses: trustabl/trustabl-action@v0` (the moving major tag).

## Notes
Expand Down
10 changes: 9 additions & 1 deletion action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ description: >-
author: trustabl
branding:
icon: shield
color: blue
color: gray-dark

inputs:
# ── what to scan ────────────────────────────────────────────────────────
Expand All @@ -26,6 +26,14 @@ inputs:
description: Pass --strict (fail on any finding regardless of severity).
required: false
default: "false"
vuln-scan:
description: >-
Pass --vuln-scan: match declared dependencies against a pinned OSV snapshot
and report known CVEs as findings (off by default). Each match becomes a
finding, so it flows through the readiness score, gating, annotations, and
SARIF. Fetches the OSV database on first use, then caches it.
required: false
default: "false"
rules-ref:
description: Pin trustabl-rules git ref. Empty = engine default.
required: false
Expand Down
Binary file added assets/github_banner.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/logo.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 4 additions & 0 deletions capabilities.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@
- **Static reliability/safety scan** for agent-SDK repos (Claude Agent SDK,
OpenAI Agents SDK, Google ADK, MCP) — runs the upstream `trustabl` binary over
your checkout, no daemon or hosted service.
- **Optional dependency CVE scan** (`vuln-scan: true`) — matches declared
dependencies against a pinned OSV snapshot and reports known CVEs as findings,
so they ride every surface (score, gate, annotations, Security tab) alongside a
dependencies-scanned / known-vulnerabilities headline.
- **node20 TypeScript action, cross-platform** — `ubuntu-*`, `macos-*`,
`windows-*` on x64/arm64; the binary is tool-cached so reruns are fast, and is
**sha256-verified** against the release `checksums.txt` before it runs.
Expand Down
56 changes: 47 additions & 9 deletions dist/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -85085,6 +85085,7 @@ function readInputs() {
version: core.getInput('version') || 'latest',
detectors: core.getInput('detectors'),
strict: core.getBooleanInput('strict'),
vulnScan: core.getBooleanInput('vuln-scan'),
rulesRef: core.getInput('rules-ref'),
rulesRepo: core.getInput('rules-repo'),
uploadSarif: core.getBooleanInput('upload-sarif'),
Expand Down Expand Up @@ -85164,11 +85165,12 @@ const RELEASE_OWNER = 'trustabl';
const RELEASE_REPO = 'trustabl';
const RELEASE_BASE = 'https://github.com/trustabl/trustabl/releases/download';
// MIN_ENGINE_VERSION is the engine release that ships --json-out/--sarif-out, the
// Code-Scanning-valid SARIF (no fixes[]), and projected_scores. Older binaries
// still work via the two-scan fallback (single-scan + headroom ladder disabled);
// we only emit a soft upgrade warning, never a hard failure.
// TODO(owner): set to the engine release tag cut with those changes.
exports.MIN_ENGINE_VERSION = '0.0.0';
// Code-Scanning-valid SARIF (no fixes[]), and projected_scores — all introduced
// together in v0.1.3. Older binaries still work via the two-scan fallback
// (single-scan + headroom ladder disabled); we only emit a soft upgrade warning,
// never a hard failure. (The v0.1.4 finding line-range shape is handled
// version-agnostically in types.ts, so it does not raise this floor.)
exports.MIN_ENGINE_VERSION = 'v0.1.3';
function binName() {
return process.platform === 'win32' ? 'trustabl.exe' : 'trustabl';
}
Expand Down Expand Up @@ -85320,6 +85322,12 @@ async function run() {
const maxSev = (0, score_1.maxSeverity)(result.findings);
const counts = (0, score_1.severityCounts)(result.findings);
const projected = result.projected_scores ? (0, score_1.projectedReadiness)(result.projected_scores) : undefined;
// Dependency headline — only when the caller opted into --vuln-scan. The vuln
// matches themselves already flow through findings (counts, gate, annotations,
// SARIF); this is the BOM/OSV summary on top.
const deps = inputs.vulnScan
? { scanned: result.dependencies?.length ?? 0, vulnerable: result.vulnerabilities?.length ?? 0 }
: undefined;
const gate = (0, gate_1.evaluateGate)({
nativeExit,
risk: riskScore,
Expand All @@ -85338,6 +85346,7 @@ async function run() {
nativeExit,
severityCounts: counts,
projected,
deps,
gate,
rulesVersion: result.rules_version,
};
Expand Down Expand Up @@ -85487,10 +85496,14 @@ function emitAnnotations(findings, max) {
const sorted = [...findings].sort((a, b) => (types_1.SEVERITY_RANK[b.severity] ?? -1) - (types_1.SEVERITY_RANK[a.severity] ?? -1));
const shown = max > 0 ? sorted.slice(0, max) : sorted;
for (const f of shown) {
const { start, end } = (0, types_1.findingLines)(f);
const props = {
title: `${f.rule_id}: ${f.title}`,
file: f.file_path || undefined,
startLine: f.line > 0 ? f.line : undefined,
startLine: start > 0 ? start : undefined,
// Span a range only for a genuine multi-line finding with a valid start; a
// single-line finding sets startLine alone (GitHub renders one line).
endLine: start > 0 && end > start ? end : undefined,
};
const msg = f.explanation || f.title || f.rule_id;
if (f.severity === 'critical' || f.severity === 'high')
Expand Down Expand Up @@ -85598,6 +85611,10 @@ function buildConsoleLines(d) {
L.push(row(`Fix +low ${p.fixMedium} -> ${p.fixLow} (+${p.fixLow - p.fixMedium})`));
L.push(row(`Fix +info ${p.fixLow} -> ${p.fixAll} (+${p.fixAll - p.fixLow})`));
}
if (d.deps) {
L.push(rule());
L.push(row(`Dependencies ${d.deps.scanned} scanned, ${d.deps.vulnerable} known vulns`));
}
L.push(rule());
L.push(row(`Max severity: ${d.maxSeverity} Native exit: ${d.nativeExit}`));
L.push(rule());
Expand Down Expand Up @@ -85719,6 +85736,10 @@ function buildSummaryMarkdown(d) {
L.push(`| Readiness score | \`${d.readiness}\` |`);
L.push(`| Risk score | \`${d.risk}\` |`);
L.push(`| Findings | \`${d.findingsCount}\` |`);
if (d.deps) {
L.push(`| Dependencies scanned | \`${d.deps.scanned}\` |`);
L.push(`| Known vulnerabilities | \`${d.deps.vulnerable}\` |`);
}
L.push(`| Max severity | \`${d.maxSeverity}\` |`);
L.push(`| Native exit | \`${d.nativeExit}\` |`);
if (d.rulesVersion)
Expand Down Expand Up @@ -85796,6 +85817,8 @@ function baseArgs(inputs) {
args.push('--detectors', inputs.detectors);
if (inputs.strict)
args.push('--strict');
if (inputs.vulnScan)
args.push('--vuln-scan');
if (inputs.rulesRef)
args.push('--rules-ref', inputs.rulesRef);
if (inputs.rulesRepo)
Expand Down Expand Up @@ -86185,17 +86208,30 @@ async function uploadSarif(token, ctx, sarifPath) {
"use strict";

// Typed view of the engine's JSON ScanResult. Ported from trustabl-vscode
// (src/types.ts) and extended with projected_scores (engine >= the release that
// added analysis.Project; optional so older binaries parse cleanly).
// (src/types.ts) and extended with projected_scores (engine >= v0.1.3) and the
// dependency BOM / OSV vulnerabilities (engine >= v0.1.4). All additive fields
// are optional so older binaries parse cleanly.
Object.defineProperty(exports, "__esModule", ({ value: true }));
exports.SEVERITY_RANK = void 0;
exports.findingLines = findingLines;
exports.parseScanResult = parseScanResult;
exports.SEVERITY_RANK = {
info: 0, low: 1, medium: 2, high: 3, critical: 4,
};
// findingLines resolves a finding's 1-indexed inclusive line range across engine
// versions: start_line/end_line (engine >= v0.1.4) with a fallback to the legacy
// single `line`. `start` is 0 for repo-scope findings with no source location, so
// callers must treat 0 as "no line". `end` is never less than `start`.
function findingLines(f) {
const start = f.start_line ?? f.line ?? 0;
const end = Math.max(start, f.end_line ?? start);
return { start, end };
}
// Tolerant parse: read only the fields we use, ignore the rest, so a future
// engine that adds fields will not break the action. projected_scores is carried
// through only when the engine emitted a complete object (all five tiers numeric).
// through only when the engine emitted a complete object (all five tiers numeric);
// dependencies/vulnerabilities stay undefined (not []) when the engine omits them,
// so a missing array is distinguishable from an empty one.
function parseScanResult(stdout) {
const data = JSON.parse(stdout);
if (data === null || typeof data !== 'object') {
Expand All @@ -86210,6 +86246,8 @@ function parseScanResult(stdout) {
surfaces: Array.isArray(data.surfaces) ? data.surfaces : [],
overall_score: data.overall_score ?? 0,
projected_scores: validProjected(data.projected_scores),
dependencies: Array.isArray(data.dependencies) ? data.dependencies : undefined,
vulnerabilities: Array.isArray(data.vulnerabilities) ? data.vulnerabilities : undefined,
coverage: data.coverage ?? { files_parsed: 0, files_skipped: 0 },
rules_version: data.rules_version ?? '',
rules_from_cache: data.rules_from_cache ?? false,
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "trustabl-action",
"version": "0.2.0",
"version": "0.3.0",
"private": true,
"description": "Static reliability/safety scanner for AI agent repos (Claude, OpenAI, Google ADK, MCP) — GitHub Action.",
"main": "dist/index.js",
Expand Down
2 changes: 2 additions & 0 deletions src/inputs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export interface Inputs {
version: string;
detectors: string;
strict: boolean;
vulnScan: boolean;
rulesRef: string;
rulesRepo: string;
uploadSarif: boolean;
Expand Down Expand Up @@ -64,6 +65,7 @@ export function readInputs(): Inputs {
version: core.getInput('version') || 'latest',
detectors: core.getInput('detectors'),
strict: core.getBooleanInput('strict'),
vulnScan: core.getBooleanInput('vuln-scan'),
rulesRef: core.getInput('rules-ref'),
rulesRepo: core.getInput('rules-repo'),
uploadSarif: core.getBooleanInput('upload-sarif'),
Expand Down
11 changes: 6 additions & 5 deletions src/install.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,12 @@ const RELEASE_REPO = 'trustabl';
const RELEASE_BASE = 'https://github.com/trustabl/trustabl/releases/download';

// MIN_ENGINE_VERSION is the engine release that ships --json-out/--sarif-out, the
// Code-Scanning-valid SARIF (no fixes[]), and projected_scores. Older binaries
// still work via the two-scan fallback (single-scan + headroom ladder disabled);
// we only emit a soft upgrade warning, never a hard failure.
// TODO(owner): set to the engine release tag cut with those changes.
export const MIN_ENGINE_VERSION = '0.0.0';
// Code-Scanning-valid SARIF (no fixes[]), and projected_scores — all introduced
// together in v0.1.3. Older binaries still work via the two-scan fallback
// (single-scan + headroom ladder disabled); we only emit a soft upgrade warning,
// never a hard failure. (The v0.1.4 finding line-range shape is handled
// version-agnostically in types.ts, so it does not raise this floor.)
export const MIN_ENGINE_VERSION = 'v0.1.3';

export interface Capabilities {
fileOut: boolean; // engine supports --json-out / --sarif-out (single-scan dual output)
Expand Down
7 changes: 7 additions & 0 deletions src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,12 @@ async function run(): Promise<void> {
const maxSev = maxSeverity(result.findings);
const counts = severityCounts(result.findings);
const projected = result.projected_scores ? projectedReadiness(result.projected_scores) : undefined;
// Dependency headline — only when the caller opted into --vuln-scan. The vuln
// matches themselves already flow through findings (counts, gate, annotations,
// SARIF); this is the BOM/OSV summary on top.
const deps = inputs.vulnScan
? { scanned: result.dependencies?.length ?? 0, vulnerable: result.vulnerabilities?.length ?? 0 }
: undefined;

const gate = evaluateGate({
nativeExit,
Expand All @@ -66,6 +72,7 @@ async function run(): Promise<void> {
nativeExit,
severityCounts: counts,
projected,
deps,
gate,
rulesVersion: result.rules_version,
};
Expand Down
Loading
Loading