Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
3456655
feat(cloud-tests): auditor visibility improvements (phases 1-5)
tofikwest May 13, 2026
188ecf0
Merge branch 'main' into tofik/cloud-tests-auditor-visibility
tofikwest May 13, 2026
c05da23
fix(cloud-tests): address cubic review findings
tofikwest May 13, 2026
d52294e
fix(cloud-tests): legacy result-filter robustness + Prisma migration …
tofikwest May 13, 2026
a9b574d
chore(cloud-tests): replace two manual migrations with one prisma-gen…
tofikwest May 13, 2026
12411c7
fix(cloud-tests): nested arrays in sanitizer + strict date-only expir…
tofikwest May 13, 2026
2f59fc2
Merge branch 'main' into tofik/cloud-tests-auditor-visibility
tofikwest May 13, 2026
7c22759
chore: merge release v3.55.2 back to main [skip ci]
github-actions[bot] May 15, 2026
160c9ac
Merge remote-tracking branch 'origin/main' into tofik/cloud-tests-aud…
tofikwest May 15, 2026
01295d7
refactor(cloud-tests): remove redundant last-scan strip
tofikwest May 15, 2026
689a47c
Merge branch 'tofik/cloud-tests-auditor-visibility' of github.com:try…
tofikwest May 15, 2026
80232bc
refactor(cloud-tests): move "Mark as exception" into expanded finding…
tofikwest May 15, 2026
11d717f
fix(cloud-tests): teach AI fix-plan to describe create-from-scratch r…
tofikwest May 15, 2026
8111faf
fix(cloud-tests): deterministic backstop for empty CURRENT/PROPOSED i…
tofikwest May 15, 2026
9faf036
fix(cloud-tests): differentiate check-definition fields + structured …
tofikwest May 15, 2026
79bfb0f
fix(cloud-tests): address cubic review on RemediationSection and enri…
tofikwest May 15, 2026
0e193c2
fix(cloud-tests): address cubic round 4 findings
tofikwest May 15, 2026
3863e83
Merge pull request #2838 from trycompai/tofik/cloud-tests-auditor-vis…
tofikwest May 15, 2026
6988e37
fix(background-check): persist exemption reason + justification on me…
tofikwest May 15, 2026
ada894d
fix(app): add new exemption fields to createMockMember
tofikwest May 15, 2026
2c2da31
Merge pull request #2863 from trycompai/tofik/bg-check-exempt-reason-…
tofikwest May 15, 2026
33042e7
fix(cloud-tests): address cubic findings on production deploy PR
tofikwest May 15, 2026
e080cd2
Merge pull request #2864 from trycompai/tofik/cubic-cloud-tests-followup
tofikwest May 15, 2026
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
189 changes: 189 additions & 0 deletions apps/api/src/cloud-security/ai-description.prompt.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
import {
buildCheckDescriptionPrompt,
checkDescriptionSchema,
findForbiddenContent,
} from './ai-description.prompt';

describe('ai-description.prompt', () => {
describe('checkDescriptionSchema', () => {
it('accepts a well-formed Tier 3 description', () => {
const parsed = checkDescriptionSchema.safeParse({
title: 'IAM password policy enforces 14+ character minimum',
description:
'Verifies that the AWS account password policy requires user passwords to be at least 14 characters long.',
passCriteria:
'Password policy exists AND MinimumPasswordLength >= 14',
failCriteria:
'No password policy is configured OR MinimumPasswordLength < 14',
whyItMatters:
'Short passwords are vulnerable to brute force attacks and credential stuffing.',
});
expect(parsed.success).toBe(true);
});

it('rejects fields below minimum length', () => {
const parsed = checkDescriptionSchema.safeParse({
title: '',
description: 'short',
passCriteria: 'x',
failCriteria: 'y',
whyItMatters: 'z',
});
expect(parsed.success).toBe(false);
});
});

describe('findForbiddenContent', () => {
const baseline = {
title: 'IAM password policy enforces 14+ character minimum',
description:
'Verifies that the AWS account password policy requires user passwords to be at least 14 characters long.',
passCriteria:
'Password policy exists AND MinimumPasswordLength >= 14',
failCriteria:
'No password policy is configured OR MinimumPasswordLength < 14',
whyItMatters:
'Short passwords are vulnerable to brute force attacks and credential stuffing.',
};

it('returns null for clean output', () => {
expect(findForbiddenContent(baseline)).toBeNull();
});

it('flags SOC 2 control numbers', () => {
expect(
findForbiddenContent({
...baseline,
whyItMatters:
'This check aligns with SOC 2 CC6.1 logical access controls.',
}),
).toMatchObject({ field: 'whyItMatters' });
});

it('flags ISO 27001 references', () => {
expect(
findForbiddenContent({
...baseline,
whyItMatters: 'Required by ISO 27001 A.9.4.3.',
}),
).toMatchObject({ field: 'whyItMatters' });
});

it('flags ISO 27001 control numbers in lowercase variants (a.5.1)', () => {
// The regex must be case-insensitive — auditors won't accept the
// model getting around the gate by lowercasing a citation.
expect(
findForbiddenContent({
...baseline,
whyItMatters: 'Maps to a.9.4.3.',
}),
).toMatchObject({ field: 'whyItMatters' });
expect(
findForbiddenContent({
...baseline,
description: 'Control a.5.1.2 enforces this.',
}),
).toMatchObject({ field: 'description' });
});

it('flags HIPAA / NIST framework citations', () => {
expect(
findForbiddenContent({
...baseline,
description: 'HIPAA-aligned password requirement.',
}),
).toMatchObject({ field: 'description' });
expect(
findForbiddenContent({
...baseline,
description: 'Maps to NIST AC-2.',
}),
).toMatchObject({ field: 'description' });
});

it('flags any URL', () => {
expect(
findForbiddenContent({
...baseline,
whyItMatters: 'See https://docs.aws.amazon.com for more info.',
}),
).toMatchObject({ field: 'whyItMatters' });
expect(
findForbiddenContent({
...baseline,
description: 'Reference: www.cisecurity.org/benchmark.',
}),
).toMatchObject({ field: 'description' });
});

it('flags CC<number>.<number> control patterns even without "SOC 2"', () => {
expect(
findForbiddenContent({
...baseline,
passCriteria: 'Control reference: CC7.1',
}),
).toMatchObject({ field: 'passCriteria' });
});

it('flags bare CIS/PCI/NIST/HIPAA control numbers (e.g. "CIS 1.8")', () => {
expect(
findForbiddenContent({
...baseline,
whyItMatters: 'Aligns with CIS 1.8 best practices.',
}),
).toMatchObject({ field: 'whyItMatters' });
expect(
findForbiddenContent({
...baseline,
whyItMatters: 'Required by PCI 8.2.3.',
}),
).toMatchObject({ field: 'whyItMatters' });
expect(
findForbiddenContent({
...baseline,
whyItMatters: 'See also: NIST AC-2.',
}),
).toMatchObject({ field: 'whyItMatters' });
expect(
findForbiddenContent({
...baseline,
whyItMatters: 'Maps to HIPAA 164.312.',
}),
).toMatchObject({ field: 'whyItMatters' });
});
});


describe('buildCheckDescriptionPrompt', () => {
it('includes provider, severity, title and description', () => {
const prompt = buildCheckDescriptionPrompt({
provider: 'aws',
serviceName: 'IAM',
title: 'IAM user "john" does not have MFA enabled',
description: 'User john has no MFA device configured.',
severity: 'high',
remediation: 'Enable MFA via IAM console.',
});
expect(prompt).toContain('AWS');
expect(prompt).toContain('IAM');
expect(prompt).toContain('high');
expect(prompt).toContain('john');
expect(prompt).toContain('Enable MFA');
});

it('omits null/empty fields cleanly', () => {
const prompt = buildCheckDescriptionPrompt({
provider: 'aws',
serviceName: null,
title: 'Untitled finding',
description: null,
severity: null,
remediation: null,
});
expect(prompt).toContain('Untitled finding');
expect(prompt).not.toContain('Service:');
expect(prompt).not.toContain('Finding description:');
expect(prompt).not.toContain('Suggested remediation:');
});
});
});
134 changes: 134 additions & 0 deletions apps/api/src/cloud-security/ai-description.prompt.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
import { z } from 'zod';

/**
* Output shape of the "About this check" panel auditors see when they
* expand a finding. Tier 3 = title, description, pass/fail criteria,
* rationale. NOTHING ELSE — no compliance control numbers, no external
* URLs, no framework claims. Server-side validation strips forbidden
* content before persisting to the cache.
*/
export const checkDescriptionSchema = z.object({
title: z
.string()
.min(1)
.max(160)
.describe('Plain-English summary of what this check verifies (~1 sentence).'),
description: z
.string()
.min(20)
.max(600)
.describe(
'What the check actually verifies in plain English (1-3 sentences). No control numbers, no URLs, no framework claims.',
),
passCriteria: z
.string()
.min(10)
.max(300)
.describe(
'The configuration condition that makes this check pass. 1 sentence.',
),
failCriteria: z
.string()
.min(10)
.max(300)
.describe(
'The configuration condition that makes this check fail. 1 sentence.',
),
whyItMatters: z
.string()
.min(20)
.max(600)
.describe(
'Security/risk rationale in plain English. 1-2 sentences. NO compliance citations.',
),
});

export type CheckDescription = z.infer<typeof checkDescriptionSchema>;

export const CHECK_DESCRIPTION_SYSTEM_PROMPT = `You write audit-friendly explanations of cloud-security checks.

Audience: an external auditor (SOC 2 / ISO 27001) reviewing the customer's environment. They need to TRUST the automation by understanding what each check verifies.

OUTPUT
- Return only the fields requested by the schema. Nothing else.
- Tone: neutral, professional, present tense, third person.
- Plain English. Avoid product jargon when a simpler word works.

HARD RULES (output that violates these is stripped server-side):
- DO NOT mention any specific compliance control number (no "SOC 2 CC6.1", "ISO 27001 A.9.4.3", "NIST AC-2", "HIPAA \xA7164.312", "CIS 1.8", "PCI 8.2.3", etc.).
- DO NOT name a compliance framework as if it requires this check (no "required by SOC 2", "ISO mandates", "HIPAA-aligned").
- DO NOT include any URL or external link.
- DO NOT invent product features the input doesn't describe.

Style: short, clear, factual.`;

export interface CheckDescriptionInput {
provider: 'aws' | 'gcp' | 'azure' | string;
serviceName: string | null;
title: string;
description: string | null;
severity: string | null;
remediation: string | null;
}

export function buildCheckDescriptionPrompt(input: CheckDescriptionInput): string {
return [
`Provider: ${input.provider.toUpperCase()}`,
input.serviceName ? `Service: ${input.serviceName}` : null,
`Severity: ${input.severity ?? 'unknown'}`,
`Finding title: "${input.title}"`,
input.description ? `Finding description: "${input.description}"` : null,
input.remediation ? `Suggested remediation: "${input.remediation}"` : null,
'',
'Generate a CheckDefinition for this check. Focus on what the CHECK as a class verifies — not on the specific resource that failed.',
]
.filter(Boolean)
.join('\n');
}

/**
* Detect content that slipped past the prompt — typical AI hallucinations
* we want to keep out of the rendered panel. Returns a list of regex
* patterns that should NEVER match in production output.
*/
const FORBIDDEN_PATTERNS: readonly RegExp[] = [
// Compliance framework names (full)
/\bSOC ?2\b/i,
/\bISO ?27001\b/i,
/\bISO ?27002\b/i,
/\bHIPAA\b/i,
/\bNIST\b/i,
/\bPCI ?DSS\b/i,
/\bCIS ?Benchmark\b/i,
// Bare control-number citations following any of the known framework
// prefixes — catches "CIS 1.8", "PCI 8.2.3", "NIST AC-2",
// "HIPAA 164.312" even without the full framework name re-mentioned.
/\b(CIS|PCI|NIST|HIPAA|HITRUST|FedRAMP) ?[A-Z]*[- ]?\d+(\.\d+){0,3}\b/i,
// SOC 2 / ISO control-number formats — case-insensitive so lowercase
// variants (e.g. "a.5.1.2") are blocked too.
/\bCC\d+\.\d+\b/i,
/\bA\.\d+\.\d+(\.\d+)?\b/i,
// URLs
/https?:\/\//i,
/www\./i,
];

/**
* Return the first forbidden pattern that matches any field's value, or
* null when output is clean. Used as a server-side backstop to the prompt.
*/
export function findForbiddenContent(
description: CheckDescription,
): { field: keyof CheckDescription; pattern: string } | null {
for (const [field, value] of Object.entries(description) as [
keyof CheckDescription,
string,
][]) {
for (const pattern of FORBIDDEN_PATTERNS) {
if (pattern.test(value)) {
return { field, pattern: pattern.source };
}
}
}
return null;
}
59 changes: 59 additions & 0 deletions apps/api/src/cloud-security/ai-description.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { Injectable, Logger } from '@nestjs/common';
import { generateObject } from 'ai';
import { anthropic } from '@ai-sdk/anthropic';
import {
CHECK_DESCRIPTION_SYSTEM_PROMPT,
buildCheckDescriptionPrompt,
checkDescriptionSchema,
findForbiddenContent,
type CheckDescription,
type CheckDescriptionInput,
} from './ai-description.prompt';

/**
* Haiku 4.5 — cheap, fast, plenty good for descriptive text. Locked here
* so cache invalidation can detect model upgrades via `modelVersion`.
*/
export const DESCRIPTION_MODEL_VERSION = 'claude-haiku-4-5';
const MODEL = anthropic(DESCRIPTION_MODEL_VERSION);

@Injectable()
export class AiDescriptionService {
private readonly logger = new Logger(AiDescriptionService.name);

/**
* Generate a Tier 3 "About this check" panel from a finding's metadata.
* Returns null on any AI failure — callers should surface a graceful
* fallback (showing only the existing per-finding description) rather
* than throwing.
*/
async generate(input: CheckDescriptionInput): Promise<CheckDescription | null> {
try {
const { object } = await generateObject({
model: MODEL,
schema: checkDescriptionSchema,
system: CHECK_DESCRIPTION_SYSTEM_PROMPT,
prompt: buildCheckDescriptionPrompt(input),
temperature: 0,
});

// Server-side backstop: if Haiku slipped past the prompt and emitted
// a compliance control number or URL, refuse to cache it. Callers
// get null and the UI falls back to existing content.
const violation = findForbiddenContent(object);
if (violation) {
this.logger.warn(
`AI description for "${input.title}" rejected: ${violation.field} matched forbidden pattern ${violation.pattern}`,
);
return null;
}

return object;
} catch (err) {
this.logger.error(
`AI description generation failed for "${input.title}": ${err instanceof Error ? err.message : String(err)}`,
);
return null;
}
}
}
Loading
Loading