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
4 changes: 2 additions & 2 deletions action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,11 @@ inputs:
required: false
default: 'warden.toml'
fail-on:
description: 'Minimum severity level to fail the action (critical, high, medium, low, info)'
description: 'Minimum severity level to fail the action (off, critical, high, medium, low, info). Use "off" to never fail.'
required: false
default: 'high'
comment-on:
description: 'Minimum severity level to show annotations in code review (critical, high, medium, low, info)'
description: 'Minimum severity level to show annotations in code review (off, critical, high, medium, low, info). Use "off" to disable comments.'
required: false
default: 'medium'
max-findings:
Expand Down
33 changes: 18 additions & 15 deletions src/action/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@ import {
} from '../output/github-checks.js';
import { matchTrigger, shouldFail, countFindingsAtOrAbove, countSeverity } from '../triggers/matcher.js';
import { resolveSkillAsync } from '../skills/loader.js';
import { filterFindingsBySeverity } from '../types/index.js';
import type { EventContext, SkillReport, UsageStats } from '../types/index.js';
import { filterFindingsBySeverity, SeverityThresholdSchema } from '../types/index.js';
import type { EventContext, SkillReport, SeverityThreshold, UsageStats } from '../types/index.js';
import type { RenderResult } from '../output/types.js';
import { processInBatches, DEFAULT_CONCURRENCY } from '../utils/index.js';

Expand All @@ -45,8 +45,8 @@ interface ActionInputs {
anthropicApiKey: string;
githubToken: string;
configPath: string;
failOn?: 'critical' | 'high' | 'medium' | 'low' | 'info';
commentOn?: 'critical' | 'high' | 'medium' | 'low' | 'info';
failOn?: SeverityThreshold;
commentOn?: SeverityThreshold;
maxFindings: number;
/** Max concurrent trigger executions */
parallel: number;
Expand Down Expand Up @@ -77,16 +77,14 @@ function getInputs(): ActionInputs {
);
}

const validSeverities = ['critical', 'high', 'medium', 'low', 'info'] as const;

const failOnInput = getInput('fail-on');
const failOn = validSeverities.includes(failOnInput as typeof validSeverities[number])
? (failOnInput as typeof validSeverities[number])
const failOn = SeverityThresholdSchema.safeParse(failOnInput).success
? (failOnInput as SeverityThreshold)
: undefined;

const commentOnInput = getInput('comment-on');
const commentOn = validSeverities.includes(commentOnInput as typeof validSeverities[number])
? (commentOnInput as typeof validSeverities[number])
const commentOn = SeverityThresholdSchema.safeParse(commentOnInput).success
? (commentOnInput as SeverityThreshold)
: undefined;

return {
Expand Down Expand Up @@ -532,10 +530,14 @@ async function run(): Promise<void> {
}
}

const renderResult = renderSkillReport(report, {
maxFindings: trigger.output.maxFindings ?? inputs.maxFindings,
commentOn,
});
// Only render if we're going to post comments
const renderResult =
commentOn !== 'off'
? renderSkillReport(report, {
maxFindings: trigger.output.maxFindings ?? inputs.maxFindings,
commentOn,
})
: undefined;

logGroupEnd();
return {
Expand Down Expand Up @@ -576,7 +578,8 @@ async function run(): Promise<void> {
if (result.report) {
reports.push(result.report);

// Post review to GitHub only if there are findings (after commentOn filtering) OR commentOnSuccess is true
// Post review to GitHub (renderResult is undefined when commentOn is 'off')
// Only post if there are findings (after commentOn filtering) OR commentOnSuccess is true
const filteredFindings = filterFindingsBySeverity(result.report.findings, result.commentOn);
const hasFindings = filteredFindings.length > 0;
const commentOnSuccess = result.commentOnSuccess ?? false;
Expand Down
16 changes: 8 additions & 8 deletions src/cli/args.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
import { parseArgs } from 'node:util';
import { z } from 'zod';
import { SeveritySchema } from '../types/index.js';
import type { Severity } from '../types/index.js';
import { SeverityThresholdSchema } from '../types/index.js';
import type { SeverityThreshold } from '../types/index.js';

export const CLIOptionsSchema = z.object({
targets: z.array(z.string()).optional(),
skill: z.string().optional(),
config: z.string().optional(),
json: z.boolean().default(false),
failOn: SeveritySchema.optional(),
failOn: SeverityThresholdSchema.optional(),
/** Only show findings at or above this severity in output */
commentOn: SeveritySchema.optional(),
commentOn: SeverityThresholdSchema.optional(),
help: z.boolean().default(false),
/** Max concurrent trigger/skill executions (default: 4) */
parallel: z.number().int().positive().optional(),
Expand Down Expand Up @@ -73,9 +73,9 @@ Options:
-m, --model <model> Model to use (fallback when not set in config)
--json Output results as JSON
--fail-on <severity> Exit with code 1 if findings >= severity
(critical, high, medium, low, info)
(off, critical, high, medium, low, info)
--comment-on <sev> Only show findings >= severity in output
(critical, high, medium, low, info)
(off, critical, high, medium, low, info)
--fix Automatically apply all suggested fixes
--parallel <n> Max concurrent trigger/skill executions (default: 4)
--quiet Errors and final summary only
Expand Down Expand Up @@ -318,8 +318,8 @@ export function parseCliArgs(argv: string[] = process.argv.slice(2)): ParsedArgs
config: values.config,
model: values.model,
json: values.json,
failOn: values['fail-on'] as Severity | undefined,
commentOn: values['comment-on'] as Severity | undefined,
failOn: values['fail-on'] as SeverityThreshold | undefined,
commentOn: values['comment-on'] as SeverityThreshold | undefined,
fix: values.fix,
force: values.force,
parallel: values.parallel ? parseInt(values.parallel, 10) : undefined,
Expand Down
6 changes: 3 additions & 3 deletions src/cli/output/tasks.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Listr } from 'listr2';
import type { ListrTask, ListrRendererValue } from 'listr2';
import type { SkillReport, Severity, Finding, UsageStats, EventContext } from '../../types/index.js';
import type { SkillReport, SeverityThreshold, Finding, UsageStats, EventContext } from '../../types/index.js';
import type { SkillDefinition } from '../../config/schema.js';
import {
prepareFiles,
Expand All @@ -22,7 +22,7 @@ import { truncate, countBySeverity, formatSeverityDot } from './formatters.js';
export interface SkillTaskResult {
name: string;
report?: SkillReport;
failOn?: Severity;
failOn?: SeverityThreshold;
error?: unknown;
}

Expand All @@ -39,7 +39,7 @@ export interface SkillTaskContext {
export interface SkillTaskOptions {
name: string;
displayName?: string;
failOn?: Severity;
failOn?: SeverityThreshold;
/** Resolve the skill definition (may be async for loading) */
resolveSkill: () => Promise<SkillDefinition>;
/** The event context with files to analyze */
Expand Down
5 changes: 3 additions & 2 deletions src/cli/terminal.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { readFileSync } from 'node:fs';
import chalk from 'chalk';
import type { SkillReport, Finding, Severity } from '../types/index.js';
import type { SkillReport, Finding, Severity, SeverityThreshold } from '../types/index.js';
import { filterFindingsBySeverity } from '../types/index.js';
import {
formatSeverityBadge,
Expand Down Expand Up @@ -235,8 +235,9 @@ function aggregateUsage(reports: SkillReport[]) {
/**
* Filter reports to only include findings at or above the given severity threshold.
* Returns new report objects with filtered findings; does not mutate the originals.
* If commentOn is 'off', returns reports with empty findings.
*/
export function filterReportsBySeverity(reports: SkillReport[], commentOn?: Severity): SkillReport[] {
export function filterReportsBySeverity(reports: SkillReport[], commentOn?: SeverityThreshold): SkillReport[] {
if (!commentOn) return reports;
return reports.map((report) => ({
...report,
Expand Down
5 changes: 3 additions & 2 deletions src/config/schema.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { z } from 'zod';
import { SeverityThresholdSchema } from '../types/index.js';

// Tool names that can be allowed/denied
export const ToolNameSchema = z.enum([
Expand Down Expand Up @@ -41,8 +42,8 @@ export type PathFilter = z.infer<typeof PathFilterSchema>;

// Output configuration per trigger
export const OutputConfigSchema = z.object({
failOn: z.enum(['critical', 'high', 'medium', 'low', 'info']).optional(),
commentOn: z.enum(['critical', 'high', 'medium', 'low', 'info']).optional(),
failOn: SeverityThresholdSchema.optional(),
commentOn: SeverityThresholdSchema.optional(),
maxFindings: z.number().int().positive().optional(),
/** Post a PR comment even when there are no findings (default: false) */
commentOnSuccess: z.boolean().optional(),
Expand Down
14 changes: 7 additions & 7 deletions src/output/github-checks.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { Octokit } from '@octokit/rest';
import { SEVERITY_ORDER, filterFindingsBySeverity } from '../types/index.js';
import type { Severity, Finding, SkillReport, UsageStats } from '../types/index.js';
import type { Severity, SeverityThreshold, Finding, SkillReport, UsageStats } from '../types/index.js';
import { formatStatsCompact, formatDuration, formatCost, formatTokens, countBySeverity } from '../cli/output/formatters.js';

/**
Expand Down Expand Up @@ -33,9 +33,9 @@ export interface CheckOptions {
* Options for updating a skill check.
*/
export interface UpdateSkillCheckOptions extends CheckOptions {
failOn?: Severity;
failOn?: SeverityThreshold;
/** Only include findings at or above this severity level in annotations */
commentOn?: Severity;
commentOn?: SeverityThreshold;
}

/**
Expand Down Expand Up @@ -94,7 +94,7 @@ export function severityToAnnotationLevel(
* Returns at most MAX_ANNOTATIONS_PER_REQUEST annotations.
* If commentOn is specified, only include findings at or above that severity.
*/
export function findingsToAnnotations(findings: Finding[], commentOn?: Severity): CheckAnnotation[] {
export function findingsToAnnotations(findings: Finding[], commentOn?: SeverityThreshold): CheckAnnotation[] {
// Filter by commentOn threshold if specified
const filtered = filterFindingsBySeverity(findings, commentOn);

Expand Down Expand Up @@ -129,14 +129,14 @@ export function findingsToAnnotations(findings: Finding[], commentOn?: Severity)
*/
export function determineConclusion(
findings: Finding[],
failOn?: Severity
failOn?: SeverityThreshold
): CheckConclusion {
if (findings.length === 0) {
return 'success';
}

if (!failOn) {
// No failure threshold, findings exist but don't cause failure
if (!failOn || failOn === 'off') {
// No failure threshold or disabled, findings exist but don't cause failure
return 'neutral';
}

Expand Down
7 changes: 5 additions & 2 deletions src/output/types.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import type { SeverityThreshold } from '../types/index.js';

export interface GitHubComment {
body: string;
path?: string;
Expand All @@ -22,6 +24,7 @@ export interface RenderOptions {
includeSuggestions?: boolean;
maxFindings?: number;
groupByFile?: boolean;
/** Only include findings at or above this severity level in rendered output */
commentOn?: 'critical' | 'high' | 'medium' | 'low' | 'info';
extraLabels?: string[];
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unused extraLabels field added to interface

Low Severity

The extraLabels field was added to the RenderOptions interface but remains unused throughout the codebase. The renderSkillReport function, which consumes these options, doesn't reference it, indicating it may be dead code.

Fix in Cursor Fix in Web

/** Only include findings at or above this severity level in rendered output. Use 'off' to disable comments. */
commentOn?: SeverityThreshold;
}
10 changes: 7 additions & 3 deletions src/triggers/matcher.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { Trigger } from '../config/schema.js';
import { SEVERITY_ORDER } from '../types/index.js';
import type { EventContext, Severity, SkillReport } from '../types/index.js';
import type { EventContext, Severity, SeverityThreshold, SkillReport } from '../types/index.js';

/** Cache for compiled glob patterns */
const globCache = new Map<string, RegExp>();
Expand Down Expand Up @@ -96,16 +96,20 @@ export function matchTrigger(trigger: Trigger, context: EventContext): boolean {

/**
* Check if a report has any findings at or above the given severity threshold.
* Returns false if failOn is 'off' (disabled).
*/
export function shouldFail(report: SkillReport, failOn: Severity): boolean {
export function shouldFail(report: SkillReport, failOn: SeverityThreshold): boolean {
if (failOn === 'off') return false;
const threshold = SEVERITY_ORDER[failOn];
Comment on lines +101 to 103
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 Missing null/undefined check before accessing SEVERITY_ORDER

The code accesses SEVERITY_ORDER[failOn] without checking if 'failOn' is a valid key in the SEVERITY_ORDER object. While the early return handles 'off', if an invalid severity value is passed, this could result in 'threshold' being undefined, leading to undefined behavior in the comparison. The same issue exists in the second function accessing SEVERITY_ORDER[f.severity] for each finding.

Suggested fix: Add validation to ensure the severity value exists in SEVERITY_ORDER before accessing it, or add type guards to prevent invalid values from being passed

Suggested change
export function shouldFail(report: SkillReport, failOn: SeverityThreshold): boolean {
if (failOn === 'off') return false;
const threshold = SEVERITY_ORDER[failOn];
if (threshold === undefined) return false;

warden: security-review

Comment on lines +101 to 103
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 Potential type confusion between Severity and SeverityThreshold

The functions now accept 'SeverityThreshold' (which presumably includes 'off') but use SEVERITY_ORDER lookup without verifying that 'off' is excluded from the lookup. If 'off' is not defined in SEVERITY_ORDER, the threshold variable will be undefined, which could lead to unexpected behavior in the comparison operations. While there's an early return for 'off', the type system now allows 'off' to reach SEVERITY_ORDER[failOn] if the check is removed or bypassed.

Suggested fix: Consider using a type guard or assertion after the 'off' check to ensure type safety, or ensure SEVERITY_ORDER explicitly handles all SeverityThreshold values including 'off'.

Suggested change
export function shouldFail(report: SkillReport, failOn: SeverityThreshold): boolean {
if (failOn === 'off') return false;
const threshold = SEVERITY_ORDER[failOn];
// Type assertion to ensure failOn is now Severity after 'off' check
const severityLevel: Severity = failOn as Severity;
const threshold = SEVERITY_ORDER[severityLevel];

warden: security-review

return report.findings.some((f) => SEVERITY_ORDER[f.severity] <= threshold);
}

/**
* Count findings at or above the given severity threshold.
* Returns 0 if failOn is 'off' (disabled).
*/
export function countFindingsAtOrAbove(report: SkillReport, failOn: Severity): number {
export function countFindingsAtOrAbove(report: SkillReport, failOn: SeverityThreshold): number {
if (failOn === 'off') return 0;
const threshold = SEVERITY_ORDER[failOn];
return report.findings.filter((f) => SEVERITY_ORDER[f.severity] <= threshold).length;
}
Expand Down
8 changes: 7 additions & 1 deletion src/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@
export const SeveritySchema = z.enum(['critical', 'high', 'medium', 'low', 'info']);
export type Severity = z.infer<typeof SeveritySchema>;

// Severity threshold for config options (includes 'off' to disable)
export const SeverityThresholdSchema = z.enum(['off', 'critical', 'high', 'medium', 'low', 'info']);
export type SeverityThreshold = z.infer<typeof SeverityThresholdSchema>;

/**
* Severity order for comparison (lower = more severe).
* Single source of truth for severity ordering across the codebase.
Expand All @@ -19,9 +23,11 @@
/**
* Filter findings to only include those at or above the given severity threshold.
* If no threshold is provided, returns all findings unchanged.
* If threshold is 'off', returns empty array (disabled).
*/
export function filterFindingsBySeverity(findings: Finding[], threshold?: Severity): Finding[] {
export function filterFindingsBySeverity(findings: Finding[], threshold?: SeverityThreshold): Finding[] {
if (!threshold) return findings;
if (threshold === 'off') return [];

Check warning on line 30 in src/types/index.ts

View check run for this annotation

@sentry/warden / warden: find-bugs

Type mismatch when accessing SEVERITY_ORDER with SeverityThreshold

The function now accepts `SeverityThreshold` which includes 'off', but `SEVERITY_ORDER` is typed as `Record<Severity, number>` which does not include 'off'. While the code handles 'off' before accessing `SEVERITY_ORDER[threshold]`, TypeScript's type system will flag this as an error because `threshold` is still typed as `SeverityThreshold` (not narrowed to `Severity`) when used as the key.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 Type mismatch when accessing SEVERITY_ORDER with SeverityThreshold

The function now accepts SeverityThreshold which includes 'off', but SEVERITY_ORDER is typed as Record<Severity, number> which does not include 'off'. While the code handles 'off' before accessing SEVERITY_ORDER[threshold], TypeScript's type system will flag this as an error because threshold is still typed as SeverityThreshold (not narrowed to Severity) when used as the key.

Suggested fix: Add a type assertion or use a type guard to narrow the threshold type after the 'off' check, making the type relationship explicit.

Suggested change
if (threshold === 'off') return [];
const thresholdOrder = SEVERITY_ORDER[threshold as Severity];

warden: find-bugs

const thresholdOrder = SEVERITY_ORDER[threshold];
Comment on lines 29 to 31
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 Type mismatch when accessing SEVERITY_ORDER with SeverityThreshold

The function now accepts SeverityThreshold which includes 'off', but SEVERITY_ORDER is typed as Record<Severity, number> which does not include 'off'. When threshold is not 'off', the code accesses SEVERITY_ORDER[threshold] which could be 'off' at the type level, causing a type error. The runtime check for 'off' happens before this access, but TypeScript may not narrow the type correctly, potentially allowing undefined to be assigned to thresholdOrder.

Suggested fix: Cast threshold to Severity after the 'off' check, or use type guard to narrow the type

Suggested change
if (!threshold) return findings;
if (threshold === 'off') return [];
const thresholdOrder = SEVERITY_ORDER[threshold];
const thresholdOrder = SEVERITY_ORDER[threshold as Severity];

warden: security-review

return findings.filter((f) => SEVERITY_ORDER[f.severity] <= thresholdOrder);
Comment on lines +28 to 32
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 Type mismatch in SEVERITY_ORDER lookup with SeverityThreshold

The function now accepts 'SeverityThreshold' which includes 'off', but line 31 attempts to look up 'threshold' in 'SEVERITY_ORDER' without checking if it's 'off' first. While there is a check for 'off' on line 30, if the order of these checks is changed or if the code is refactored, this could cause a runtime error since 'SEVERITY_ORDER' is typed as 'Record<Severity, number>' and 'off' is not a valid 'Severity'. TypeScript should catch this, but the logic could be more robust.

Suggested fix: Add explicit type narrowing or use type guards to ensure 'threshold' is a valid Severity before SEVERITY_ORDER lookup. Alternatively, restructure the checks to make the intent clearer.

Suggested change
export function filterFindingsBySeverity(findings: Finding[], threshold?: SeverityThreshold): Finding[] {
if (!threshold) return findings;
if (threshold === 'off') return [];
const thresholdOrder = SEVERITY_ORDER[threshold];
return findings.filter((f) => SEVERITY_ORDER[f.severity] <= thresholdOrder);
export function filterFindingsBySeverity(findings: Finding[], threshold?: SeverityThreshold): Finding[] {
if (!threshold) return findings;
if (threshold === 'off') return [];
// At this point, threshold is guaranteed to be a valid Severity
const thresholdOrder = SEVERITY_ORDER[threshold as Severity];

warden: security-review

}
Expand Down