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
Binary file modified apps/api-service/src/analyzer/analyzer.service.ts
Binary file not shown.
57 changes: 56 additions & 1 deletion apps/api-service/src/analyzer/dto/analyze-request.dto.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,52 @@
import { IsString, IsNotEmpty, IsOptional } from "class-validator";
import { IsString, IsNotEmpty, IsOptional, IsObject, IsIn, IsInt, Min, IsArray, ValidateNested, IsBoolean } from "class-validator";
import { Type } from "class-transformer";

export const POLICY_SEVERITIES = ["critical", "high", "medium", "low", "info"] as const;
export type PolicySeverity = (typeof POLICY_SEVERITIES)[number];

/**
* A single policy-as-code rule. A build fails when any finding matches the
* rule's criteria (severity, rule id, path prefix) and the optional count
* threshold is reached.
*/
export class PolicyRuleDto {
@IsString()
@IsNotEmpty()
name: string;

@IsOptional()
@IsIn(POLICY_SEVERITIES as unknown as string[])
severity?: PolicySeverity;

@IsOptional()
@IsString()
ruleId?: string;

@IsOptional()
@IsString()
pathPrefix?: string;

/** Fail when the number of matching findings is >= this value (default 1). */
@IsOptional()
@IsInt()
@Min(1)
maxFindings?: number = 1;
}

/**
* Policy configuration that controls whether a scan result should fail a build.
*/
export class PolicyConfigDto {
@IsOptional()
@IsBoolean()
enabled?: boolean = true;

@IsOptional()
@IsArray()
@ValidateNested({ each: true })
@Type(() => PolicyRuleDto)
rules?: PolicyRuleDto[] = [];
}

export class AnalyzeRequestDto {
@IsString()
Expand All @@ -8,4 +56,11 @@ export class AnalyzeRequestDto {
@IsString()
@IsOptional()
source?: string = "remote-analysis";

/** Optional policy-as-code configuration evaluated against the analysis findings. */
@IsOptional()
@IsObject()
@ValidateNested()
@Type(() => PolicyConfigDto)
policy?: PolicyConfigDto;
}
48 changes: 48 additions & 0 deletions apps/api-service/src/analyzer/interfaces/analyzer.interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,51 @@ export interface AnalysisReport {
storageSavings: StorageSavings;
recommendations: string[];
}

/**
* Severity levels used by policy-as-code evaluation.
*/
export type PolicySeverity = "critical" | "high" | "medium" | "low" | "info";

/**
* A single policy condition set. A policy fails the build when any configured
* condition is matched by one or more violations in the report.
*/
export interface PolicyRule {
/** Human-readable identifier for the policy, used in evaluation output. */
name: string;
/** Fail when any violation has a severity at or above this level. */
failOnSeverity?: PolicySeverity;
/** Fail when a violation matches one of these rule identifiers. */
ruleIds?: string[];
/** Fail when a violation's source path matches one of these glob-style patterns. */
paths?: string[];
/** Fail when the total number of matching violations meets or exceeds this count. */
maxFindings?: number;
}

/**
* Top-level policy configuration document (e.g. loaded from a policy file).
*/
export interface PolicyConfig {
version: 1;
policies: PolicyRule[];
}

/**
* Result of evaluating a single policy against an analysis report.
*/
export interface PolicyEvaluationResult {
policy: string;
passed: boolean;
matchedViolations: FormattedViolation[];
reasons: string[];
}

/**
* Aggregate result of evaluating all policies in a configuration.
*/
export interface PolicyEvaluationReport {
passed: boolean;
results: PolicyEvaluationResult[];
}
76 changes: 76 additions & 0 deletions apps/api-service/src/rules/interfaces/rules.interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,79 @@ export interface RuleDefinition {
enabled: boolean;
documentationUrl?: string;
}

/**
* A single finding produced by rule evaluation.
*/
export interface RuleFinding {
rule: string;
severity: RuleSeverity;
category: RuleCategory;
path: string;
message: string;
}

/**
* Policy-as-code configuration. A policy fails the build when any of the
* configured thresholds are exceeded.
*/
export interface PolicyConfig {
/** Human-readable name for the policy, used in reports. */
name: string;
/** Optional description of what the policy enforces. */
description?: string;
/** Whether this policy is active. Defaults to true when omitted. */
enabled?: boolean;
/**
* Maximum number of findings allowed per severity level. A value of
* `undefined` means no limit for that severity. A limit of 0 fails on any
* finding of that severity.
*/
maxFindingsBySeverity?: Partial<Record<RuleSeverity, number>>;
/** Maximum total number of findings across all severities. */
maxTotalFindings?: number;
/** Rule names that must cause the build to fail when triggered. */
failOnRules?: string[];
/** Rule names to ignore when evaluating this policy. */
ignoreRules?: string[];
/**
* Glob-style path patterns. Findings whose path matches any of these
* patterns are excluded from evaluation.
*/
ignorePaths?: string[];
/**
* Glob-style path patterns. When set, only findings whose path matches at
* least one pattern are considered.
*/
includePaths?: string[];
}

/**
* Top-level policy file structure (e.g. `gasguard.policy.json`).
*/
export interface PolicyFile {
version: 1;
policies: PolicyConfig[];
}

/**
* Result of evaluating a single policy against a set of findings.
*/
export interface PolicyViolation {
policy: string;
reason:
| "severity-threshold"
| "total-threshold"
| "rule-triggered";
detail: string;
findings: RuleFinding[];
}

/**
* Aggregate result of evaluating all policies.
*/
export interface PolicyEvaluationResult {
passed: boolean;
violations: PolicyViolation[];
evaluatedAt: string;
}
50 changes: 49 additions & 1 deletion apps/api-service/src/rules/rules.controller.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Controller, Get, Param } from "@nestjs/common";
import { Body, Controller, Get, Param, Post } from "@nestjs/common";
import { RulesService } from "./rules.service";
import { RuleDefinition } from "./interfaces/rules.interface";

Expand All @@ -15,4 +15,52 @@ export class RulesController {
getRule(@Param("name") name: string): RuleDefinition | undefined {
return this.rulesService.getRule(name);
}

@Post("evaluate-policy")
evaluatePolicy(
@Body()
body: {
policy: {
failOn?: {
severities?: string[];
rules?: string[];
paths?: string[];
minFindingCount?: number;
};
};
findings: Array<{
severity: string;
rule: string;
path: string;
}>;
},
): { passed: boolean; violations: string[] } {
const failOn = body?.policy?.failOn ?? {};
const findings = Array.isArray(body?.findings) ? body.findings : [];
const violations: string[] = [];

const matched = findings.filter((f) => {
const sevMatch =
!failOn.severities?.length || failOn.severities.includes(f.severity);
const ruleMatch = !failOn.rules?.length || failOn.rules.includes(f.rule);
const pathMatch =
!failOn.paths?.length ||
failOn.paths.some((p) => f.path.startsWith(p));
return sevMatch && ruleMatch && pathMatch;
});

if (matched.length > 0) {
violations.push(`${matched.length} finding(s) match failure criteria`);
}
if (
typeof failOn.minFindingCount === "number" &&
matched.length >= failOn.minFindingCount
) {
violations.push(
`finding count ${matched.length} reached threshold ${failOn.minFindingCount}`,
);
}

return { passed: violations.length === 0, violations };
}
}
Binary file modified apps/api-service/src/rules/rules.service.ts
Binary file not shown.
2 changes: 1 addition & 1 deletion apps/api-service/src/scanner/dto/scan-request.dto.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { IsString, IsNotEmpty, IsOptional, MaxLength } from "class-validator";

export const MAX_SCAN_CODE_BYTES = 1MB * 1024 * 1024;
export const MAX_SCAN_CODE_BYTES = 1 * 1024 * 1024;

export class ScanRequestDto {
@IsString()
Expand Down
Loading
Loading