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
17 changes: 17 additions & 0 deletions apps/api/src/cloud-security/ai-description.prompt.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,23 @@ describe('ai-description.prompt', () => {
).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({
Expand Down
5 changes: 3 additions & 2 deletions apps/api/src/cloud-security/ai-description.prompt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,9 +104,10 @@ const FORBIDDEN_PATTERNS: readonly RegExp[] = [
// 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
// 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/,
/\bA\.\d+\.\d+(\.\d+)?\b/i,
// URLs
/https?:\/\//i,
/www\./i,
Expand Down
18 changes: 17 additions & 1 deletion apps/api/src/cloud-security/exception-expiry.utils.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,9 +75,25 @@ describe('parseExceptionExpiry', () => {
);
});

it('accepts strict ISO 8601 timestamps via the fallback', () => {
it('accepts strict ISO 8601 timestamps with an explicit timezone offset', () => {
expect(parseExceptionExpiry('2026-08-13T23:59:59Z')).not.toBeNull();
expect(parseExceptionExpiry('2026-08-13T23:59:59.999Z')).not.toBeNull();
expect(parseExceptionExpiry('2026-08-13T23:59:59+02:00')).not.toBeNull();
expect(parseExceptionExpiry('2026-08-13T23:59:59-07:00')).not.toBeNull();
});

it('rejects timestamps without an explicit timezone offset', () => {
// No `Z`, no `+02:00` etc. — `new Date()` would parse these in server
// local time, giving inconsistent expiries across environments. Force
// the caller to commit to a timezone explicitly.
expect(() => parseExceptionExpiry('2026-08-13T23:59:59')).toThrow(
BadRequestException,
);
expect(() => parseExceptionExpiry('2026-08-13T23:59')).toThrow(
BadRequestException,
);
expect(() =>
parseExceptionExpiry('2026-08-13T23:59:59.999'),
).toThrow(BadRequestException);
});
});
8 changes: 5 additions & 3 deletions apps/api/src/cloud-security/exception-expiry.utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,12 +45,14 @@ export function parseExceptionExpiry(

// Reject anything that isn't strict ISO 8601 — `new Date()` happily parses
// locale-specific strings like "January 1, 2026" and "2026/08/13", which
// would silently bypass the documented contract.
// would silently bypass the documented contract. The timezone offset is
// REQUIRED — without it, `new Date("2026-08-13T23:59:59")` is parsed in
// server-local time, giving different expiries on UTC vs Pacific hosts.
const ISO_8601 =
/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}(:\d{2})?(\.\d+)?(Z|[+-]\d{2}:?\d{2})?$/;
/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}(:\d{2})?(\.\d+)?(Z|[+-]\d{2}:?\d{2})$/;
if (!ISO_8601.test(input)) {
throw new BadRequestException(
'expiresAt must be a valid ISO date or YYYY-MM-DD calendar date.',
'expiresAt must be a YYYY-MM-DD calendar date or an ISO 8601 timestamp with an explicit timezone offset (e.g. "2026-08-13T23:59:59Z").',
);
}
const parsed = new Date(input);
Expand Down
3 changes: 1 addition & 2 deletions apps/api/src/cloud-security/reconciliation.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -124,8 +124,7 @@ export class CloudReconciliationService {

// Resolutions: prior failed → current absent or passed.
for (const [key, prior] of priorMap.entries()) {
if (!prior.passed === false) continue; // only interested in prior failures
if (prior.passed) continue;
if (prior.passed) continue; // only interested in prior failures

const current = currentMap.get(key);
if (current && !current.passed) continue; // still failing
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -455,7 +455,12 @@ export function CloudTestsSection({
if (!batchServiceId) return null;
const group = serviceGroups.find((g) => g.serviceId === batchServiceId);
if (!group) return null;
// `group.findings` is the merged failed+passed set — restrict the batch
// to failing findings only. `canFixFinding` doesn't gate on status, so
// without this filter a passing check could be targeted by "Fix All".
const fixable = group.findings.filter((f) => {
const isFailed = f.status === 'failed' || f.status === 'FAILED';
if (!isFailed) return false;
const match = canFixFinding(f);
return match?.key && match.enabled;
});
Expand Down
Loading