Skip to content

Commit 6059fcc

Browse files
authored
Merge pull request #2875 from trycompai/tofik/fix-sechub-compliance-regex-and-dto-source
fix(cloud-tests): parse all SecHub compliance formats + dedupe scan-mode literals
2 parents 66e16bb + a5d1539 commit 6059fcc

5 files changed

Lines changed: 93 additions & 13 deletions

File tree

‎apps/api/src/cloud-security/aws-scan-mode.spec.ts‎

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import {
2+
AWS_SCAN_MODES,
23
DEFAULT_AWS_SCAN_MODE,
34
isSecurityHubMode,
45
resolveAwsScanMode,
@@ -48,4 +49,29 @@ describe('aws-scan-mode', () => {
4849
expect(DEFAULT_AWS_SCAN_MODE).toBe('comp_scanners');
4950
});
5051
});
52+
53+
describe('AWS_SCAN_MODES', () => {
54+
it('lists exactly the two known modes (source of truth for DTOs / validators)', () => {
55+
// If a new mode is added, this test must change in lockstep with
56+
// the AwsScanMode union — the DTO `@IsIn(AWS_SCAN_MODES)` will
57+
// automatically pick up the new value, but reviewers should see
58+
// this test fail as a sanity check that the list was updated.
59+
expect([...AWS_SCAN_MODES]).toEqual(['comp_scanners', 'security_hub']);
60+
});
61+
62+
it('contains the default mode', () => {
63+
// Guards against future refactors that might remove the default
64+
// from the list — would break validation for every existing
65+
// connection that lacks an explicit mode.
66+
expect([...AWS_SCAN_MODES]).toContain(DEFAULT_AWS_SCAN_MODE);
67+
});
68+
69+
it('every entry passes resolveAwsScanMode round-trip (self-consistency)', () => {
70+
// Catches drift if someone adds a mode to the array without
71+
// updating resolveAwsScanMode.
72+
for (const mode of AWS_SCAN_MODES) {
73+
expect(resolveAwsScanMode(mode)).toBe(mode);
74+
}
75+
});
76+
});
5177
});

‎apps/api/src/cloud-security/aws-scan-mode.ts‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,16 @@
2323
*/
2424
export type AwsScanMode = 'comp_scanners' | 'security_hub';
2525

26+
/** Canonical list of valid scan modes. Exported so DTOs, validators,
27+
* and tests reference ONE array instead of duplicating the string
28+
* literals everywhere. If a new mode is added, only this file changes
29+
* and all importers automatically pick it up — that's the
30+
* "single source of truth" promise this module makes. */
31+
export const AWS_SCAN_MODES = [
32+
'comp_scanners',
33+
'security_hub',
34+
] as const satisfies readonly AwsScanMode[];
35+
2636
/** Default behavior for AWS connections with no scan-mode set (including
2737
* every pre-feature connection that already exists in production). */
2838
export const DEFAULT_AWS_SCAN_MODE: AwsScanMode = 'comp_scanners';
Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,25 @@
11
import { ApiProperty } from '@nestjs/swagger';
22
import { IsIn, IsString } from 'class-validator';
3-
import type { AwsScanMode } from '../aws-scan-mode';
3+
import { AWS_SCAN_MODES, type AwsScanMode } from '../aws-scan-mode';
44

55
/**
66
* Request body for `PATCH /v1/cloud-security/connections/:id/scan-mode`.
77
*
88
* Only AWS connections accept this; the service layer validates the
99
* connection is AWS before applying the change.
10+
*
11+
* The accepted values + Swagger enum both reference `AWS_SCAN_MODES`
12+
* directly so this DTO can't drift from the source of truth — adding a
13+
* new mode in `aws-scan-mode.ts` automatically widens what's accepted
14+
* here.
1015
*/
1116
export class UpdateAwsScanModeDto {
1217
@ApiProperty({
1318
description: 'Which scan engine to use for this AWS connection.',
14-
enum: ['comp_scanners', 'security_hub'],
19+
enum: AWS_SCAN_MODES,
1520
example: 'security_hub',
1621
})
1722
@IsString()
18-
@IsIn(['comp_scanners', 'security_hub'])
23+
@IsIn([...AWS_SCAN_MODES])
1924
mode!: AwsScanMode;
2025
}

‎apps/api/src/cloud-security/providers/aws/security-hub.adapter.spec.ts‎

Lines changed: 33 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -88,12 +88,42 @@ describe('security-hub.adapter helpers', () => {
8888
expect(formatRelatedRequirements([])).toBe('');
8989
});
9090

91-
it('formats a NIST 800-53 requirement in parser-compatible form', () => {
92-
expect(formatRelatedRequirements(['NIST.800-53.r5 AC-2'])).toMatch(
93-
/nist.*AC-2/i,
91+
it('emits NIST 800-53 verbatim when no explicit version separator is present', () => {
92+
// NIST embeds the revision in the standard name ("NIST.800-53.r5"),
93+
// so the structured `standard version (control)` regex doesn't
94+
// match — fallback emits the raw string and the parser surfaces it
95+
// as a single chip label. Better than fabricating a placeholder.
96+
expect(formatRelatedRequirements(['NIST.800-53.r5 AC-2'])).toBe(
97+
'NIST.800-53.r5 AC-2',
9498
);
9599
});
96100

101+
it('formats CIS AWS Foundations Benchmark in parser-compatible form (regex must accept lowercase)', () => {
102+
// Cubic P2 regression guard — the prior regex `[A-Z][A-Z0-9 .]+?`
103+
// could not match "Foundations" / "Benchmark" because of the
104+
// lowercase letters, so this format silently fell through.
105+
const result = formatRelatedRequirements([
106+
'CIS AWS Foundations Benchmark v1.2.0 1.1',
107+
]);
108+
expect(result).toMatch(/^cis .*1\.2\.0 \(1\.1\)$/);
109+
});
110+
111+
it('formats PCI DSS in parser-compatible form', () => {
112+
const result = formatRelatedRequirements(['PCI DSS v3.2.1 8.2.3']);
113+
expect(result).toBe('pci dss 3.2.1 (8.2.3)');
114+
});
115+
116+
it('formats AWS FSBP in parser-compatible form, handling the slash separator', () => {
117+
// Cubic P2 regression guard — `/` between version and control is
118+
// unique to AWS Foundational Security Best Practices. Without the
119+
// pre-normalization, the regex `\s+` between version and control
120+
// would never match.
121+
const result = formatRelatedRequirements([
122+
'AWS Foundational Security Best Practices v1.0.0/EC2.2',
123+
]);
124+
expect(result).toMatch(/^aws fsbp 1\.0\.0 \(EC2\.2\)$/);
125+
});
126+
97127
it('joins multiple requirements with "; " so the parser splits them correctly', () => {
98128
const result = formatRelatedRequirements([
99129
'NIST.800-53.r5 AC-2',

‎apps/api/src/cloud-security/providers/aws/security-hub.adapter.ts‎

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -292,20 +292,29 @@ function formatSingleRequirement(requirement: string): string {
292292
// "CIS AWS Foundations Benchmark v1.2.0 1.1"
293293
// "PCI DSS v3.2.1 8.2.3"
294294
// "AWS Foundational Security Best Practices v1.0.0/EC2.2"
295-
// We try a few patterns then fall back to a sensible default so we
296-
// surface SOMETHING rather than drop a real compliance mapping.
295+
//
296+
// AWS FSBP uses `/` between the version and the control id; normalize
297+
// it to whitespace first so the same regex handles all four formats.
298+
const normalized = cleaned.replace(/\//g, ' ');
297299

298-
const standardMatch = cleaned.match(
299-
/^([A-Z][A-Z0-9 .]+?)(?:\s+v?([\d.]+(?:[a-z]\d*)?))?\s+([A-Za-z0-9.\-]+)$/,
300+
// Match `STANDARD vVERSION CONTROL`. The first group must accept
301+
// lowercase letters and hyphens — without them, 3 of the 4 documented
302+
// formats (NIST, CIS, AWS FSBP) fall through to the raw fallback and
303+
// never produce structured `standard version (control)` output for
304+
// the compliance chips.
305+
const standardMatch = normalized.match(
306+
/^([A-Za-z][A-Za-z0-9 .\-]+?)\s+v?([\d.]+(?:[a-z]\d*)?)\s+([A-Za-z0-9.\-]+)$/,
300307
);
301308
if (standardMatch) {
302309
const [, rawStandard, version, control] = standardMatch;
303310
const standard = normalizeStandardName(rawStandard);
304-
const ver = version ?? 'unspecified';
305-
return `${standard} ${ver} (${control})`;
311+
return `${standard} ${version} (${control})`;
306312
}
307313

308-
// Fallback — keep the raw string so we don't silently drop data.
314+
// Fallback — keep the raw string for formats without an explicit
315+
// version (e.g. NIST 800-53 where the revision is embedded in the
316+
// standard name: "NIST.800-53.r5 AC-2"). The downstream parser
317+
// surfaces it as a single chip label, which is still informative.
309318
return cleaned;
310319
}
311320

0 commit comments

Comments
 (0)