Add comprehensive tests for PathConfidenceScorer, PathEnumerator, ShellSymbolicExecutor, and SymbolicState

- Implemented unit tests for PathConfidenceScorer to evaluate path scoring under various conditions, including empty constraints, known and unknown constraints, environmental dependencies, and custom weights.
- Developed tests for PathEnumerator to ensure correct path enumeration from simple scripts, handling known environments, and respecting maximum paths and depth limits.
- Created tests for ShellSymbolicExecutor to validate execution of shell scripts, including handling of commands, branching, and environment tracking.
- Added tests for SymbolicState to verify state management, variable handling, constraint addition, and environment dependency collection.
This commit is contained in:
StellaOps Bot
2025-12-20 14:03:31 +02:00
parent 0ada1b583f
commit ce8cdcd23d
71 changed files with 12438 additions and 3349 deletions

View File

@@ -41,20 +41,20 @@ describe('EvidenceDrawerComponent', () => {
] as ProofNode[],
proofRootHash: 'sha256:rootabc123',
reachabilityPath: {
nodes: [
{ id: 'entry', label: 'BillingController.Pay', type: 'entrypoint' },
{ id: 'mid', label: 'StripeClient.Create', type: 'call' },
{ id: 'sink', label: 'HttpClient.Post', type: 'sink' },
callPath: [
{ nodeId: 'mid', symbol: 'StripeClient.Create' },
],
edges: [
{ from: 'entry', to: 'mid' },
{ from: 'mid', to: 'sink' },
gates: [
{ gateType: 'auth', symbol: 'JwtMiddleware.Authenticate', confidence: 0.95, description: 'JWT required' },
{ gateType: 'rate-limit', symbol: 'RateLimiter.Check', confidence: 0.90, description: '100 req/min' },
],
entrypoint: { nodeId: 'entry', symbol: 'BillingController.Pay' },
sink: { nodeId: 'sink', symbol: 'HttpClient.Post' },
},
confidenceTier: 'high',
gates: [
{ kind: 'auth', passed: true, message: 'JWT required' },
{ kind: 'rate_limit', passed: true, message: '100 req/min' },
{ gateType: 'auth', symbol: 'JwtMiddleware.Authenticate', confidence: 0.95, description: 'JWT required' },
{ gateType: 'rate-limit', symbol: 'RateLimiter.Check', confidence: 0.90, description: '100 req/min' },
],
vexDecisions: [
{

View File

@@ -303,10 +303,13 @@ export class FindingListComponent {
*/
readonly showSummary = input<boolean>(true);
/**
* Threshold for enabling virtual scroll (number of items).
*/
readonly virtualScrollThreshold = input<number>(50);
// NOTE: Virtual scrolling requires @angular/cdk package.
// These inputs are kept for future implementation but currently unused.
// readonly useVirtualScroll = input<boolean>(true);
// readonly virtualScrollThreshold = input<number>(50);
// readonly itemHeight = input<number>(64);
// readonly viewportHeight = input<number>(400);
@@ -390,6 +393,23 @@ export class FindingListComponent {
return `Vulnerability findings list, ${count} item${count === 1 ? '' : 's'}`;
});
/**
* Count of critical (≥9.0) and high (≥7.0) severity findings.
*/
criticalHighCount(): number {
return this.sortedFindings().filter(f => {
const score = f.score_explain?.risk_score ?? 0;
return score >= 7.0;
}).length;
}
/**
* Whether to use virtual scroll based on threshold.
*/
useVirtualScroll(): boolean {
return this.sortedFindings().length >= this.virtualScrollThreshold();
}
/**
* Track by function for ngFor.
*/

View File

@@ -410,6 +410,11 @@ export class FindingRowComponent {
*/
readonly showChainStatus = input<boolean>(true);
/**
* Maximum number of path steps to show in preview (default: 5).
*/
readonly maxPathSteps = input<number>(5);
/**
* Emitted when user clicks to view evidence details.
*/
@@ -502,6 +507,116 @@ export class FindingRowComponent {
return `${cve} in ${component}, risk score ${score.toFixed(1)}`;
});
// =========================================================================
// Boundary & Exposure Computed Properties
// =========================================================================
/**
* Check if boundary is internet-facing.
*/
isInternetFacing(): boolean {
return this.finding()?.boundary?.exposure?.internet_facing === true;
}
/**
* Check if auth is required for the boundary.
*/
hasAuthRequired(): boolean {
return this.finding()?.boundary?.auth?.required === true;
}
/**
* Format the boundary surface info.
*/
boundarySurface(): string {
const surface = this.finding()?.boundary?.surface;
if (!surface) return '';
const parts: string[] = [];
if (surface.protocol) parts.push(surface.protocol);
if (surface.port !== undefined) parts.push(String(surface.port));
if (surface.type) parts.push(surface.type);
return parts.join(':');
}
// =========================================================================
// Entrypoint Computed Properties
// =========================================================================
/**
* Get the entrypoint type.
*/
entrypointType(): string {
return this.finding()?.entrypoint?.type ?? '';
}
/**
* Format entrypoint route with method.
*/
entrypointRoute(): string {
const entry = this.finding()?.entrypoint;
if (!entry) return '';
if (entry.method && entry.route) {
return `${entry.method} ${entry.route}`;
}
return entry.route ?? '';
}
// =========================================================================
// Path Preview Computed Properties
// =========================================================================
/**
* Get truncated path preview based on maxPathSteps.
*/
pathPreview(): readonly string[] {
const path = this.callPath();
const max = this.maxPathSteps();
if (path.length <= max) return path;
return path.slice(0, max);
}
/**
* Check if path was truncated.
*/
pathTruncated(): boolean {
return this.callPath().length > this.maxPathSteps();
}
// =========================================================================
// Staleness Computed Properties
// =========================================================================
/**
* Check if evidence has expired (is stale).
*/
isStale(): boolean {
const expiresAt = this.finding()?.expires_at;
if (!expiresAt) return false;
try {
const expiryDate = new Date(expiresAt);
return expiryDate < new Date();
} catch {
return false;
}
}
/**
* Check if evidence is near expiry (within 24 hours).
*/
isNearExpiry(): boolean {
const expiresAt = this.finding()?.expires_at;
if (!expiresAt) return false;
try {
const expiryDate = new Date(expiresAt);
const now = new Date();
const twentyFourHoursFromNow = new Date(now.getTime() + 24 * 60 * 60 * 1000);
return expiryDate > now && expiryDate <= twentyFourHoursFromNow;
} catch {
return false;
}
}
// =========================================================================
// Computed Descriptions
// =========================================================================