diff --git a/libs/hdf-converters/sample_jsons/sarif_mapper/sample_input_report/semgrep_sarif.sarif b/libs/hdf-converters/sample_jsons/sarif_mapper/sample_input_report/semgrep_sarif.sarif
new file mode 100644
index 0000000000..f15a4e4f7a
--- /dev/null
+++ b/libs/hdf-converters/sample_jsons/sarif_mapper/sample_input_report/semgrep_sarif.sarif
@@ -0,0 +1,99 @@
+{
+ "version": "2.1.0",
+ "runs": [
+ {
+ "invocations": [
+ {
+ "executionSuccessful": true,
+ "toolExecutionNotifications": [
+ {
+ "descriptor": {
+ "id": "Syntax error"
+ },
+ "level": "warning",
+ "message": {
+ "text": "Syntax error at line .github/workflows/sonarqube-hdf-emit.yml:234:\n When parsing a snippet as Bash for metavariable-pattern in rule 'yaml.github-actions.security.curl-eval.curl-eval', `{ steps.aws.outcome` was unexpected"
+ }
+ },
+ {
+ "descriptor": {
+ "id": "Syntax error"
+ },
+ "level": "warning",
+ "message": {
+ "text": "Syntax error at line .github/workflows/sonarqube-hdf-emit.yml:234:\n When parsing a snippet as Bash for metavariable-pattern in rule 'yaml.github-actions.security.gha-curl-pipe-shell.gha-curl-pipe-shell', `{ steps.aws.outcome` was unexpected"
+ }
+ }
+ ]
+ }
+ ],
+ "results": [
+ {
+ "fingerprints": {
+ "matchBasedId/v1": "requires login"
+ },
+ "locations": [
+ {
+ "physicalLocation": {
+ "artifactLocation": {
+ "uri": "tools/nist_catalog_to_inspec/generate.py",
+ "uriBaseId": "%SRCROOT%"
+ },
+ "region": {
+ "endColumn": 54,
+ "endLine": 168,
+ "snippet": {
+ "text": " with urllib.request.urlopen(ref, timeout=180) as resp: # noqa: S310"
+ },
+ "startColumn": 14,
+ "startLine": 168
+ }
+ }
+ }
+ ],
+ "message": {
+ "text": "Detected a dynamic value being used with urllib. urllib supports 'file://' schemes, so a dynamic value controlled by a malicious actor may allow them to read arbitrary files. Audit uses of urllib calls to ensure user data cannot control the URLs, or consider using the 'requests' library instead."
+ },
+ "properties": {},
+ "ruleId": "python.lang.security.audit.dynamic-urllib-use-detected.dynamic-urllib-use-detected"
+ }
+ ],
+ "tool": {
+ "driver": {
+ "name": "Semgrep OSS",
+ "rules": [
+ {
+ "defaultConfiguration": {
+ "level": "warning"
+ },
+ "fullDescription": {
+ "text": "Detected a dynamic value being used with urllib. urllib supports 'file://' schemes, so a dynamic value controlled by a malicious actor may allow them to read arbitrary files. Audit uses of urllib calls to ensure user data cannot control the URLs, or consider using the 'requests' library instead."
+ },
+ "help": {
+ "markdown": "Detected a dynamic value being used with urllib. urllib supports 'file://' schemes, so a dynamic value controlled by a malicious actor may allow them to read arbitrary files. Audit uses of urllib calls to ensure user data cannot control the URLs, or consider using the 'requests' library instead.\n\n#### \ud83d\udc8e Enable cross-file analysis and Pro rules for free at sg.run/pro\n\nReferences:\n - [Semgrep Rule](https://semgrep.dev/r/python.lang.security.audit.dynamic-urllib-use-detected.dynamic-urllib-use-detected)\n - [https://cwe.mitre.org/data/definitions/939.html](https://cwe.mitre.org/data/definitions/939.html)\n",
+ "text": "Detected a dynamic value being used with urllib. urllib supports 'file://' schemes, so a dynamic value controlled by a malicious actor may allow them to read arbitrary files. Audit uses of urllib calls to ensure user data cannot control the URLs, or consider using the 'requests' library instead.\n\ud83d\udc8e Enable cross-file analysis and Pro rules for free at sg.run/pro"
+ },
+ "helpUri": "https://semgrep.dev/r/python.lang.security.audit.dynamic-urllib-use-detected.dynamic-urllib-use-detected",
+ "id": "python.lang.security.audit.dynamic-urllib-use-detected.dynamic-urllib-use-detected",
+ "name": "python.lang.security.audit.dynamic-urllib-use-detected.dynamic-urllib-use-detected",
+ "properties": {
+ "precision": "very-high",
+ "tags": [
+ "CWE-939: Improper Authorization in Handler for Custom URL Scheme",
+ "LOW CONFIDENCE",
+ "OWASP-A01:2017 - Injection",
+ "security"
+ ]
+ },
+ "shortDescription": {
+ "text": "Semgrep Finding: python.lang.security.audit.dynamic-urllib-use-detected.dynamic-urllib-use-detected"
+ }
+ }
+ ],
+ "semanticVersion": "1.174.0"
+ }
+ }
+ }
+ ],
+ "$schema": "https://docs.oasis-open.org/sarif/sarif/v2.1.0/os/schemas/sarif-schema-2.1.0.json"
+}
\ No newline at end of file
diff --git a/libs/hdf-converters/sample_jsons/sarif_mapper/sarif-hdf-withraw.json b/libs/hdf-converters/sample_jsons/sarif_mapper/sarif-hdf-withraw.json
index 386b5fe9de..91d1159c34 100644
--- a/libs/hdf-converters/sample_jsons/sarif_mapper/sarif-hdf-withraw.json
+++ b/libs/hdf-converters/sample_jsons/sarif_mapper/sarif-hdf-withraw.json
@@ -1,10 +1,10 @@
{
"platform": {
"name": "Heimdall Tools",
- "release": "2.6.29",
+ "release": "2.12.6",
"target_id": "Static Analysis Results Interchange Format"
},
- "version": "2.6.29",
+ "version": "2.12.6",
"statistics": {},
"profiles": [
{
@@ -19,15 +19,14 @@
{
"tags": {
"cci": [
- "CCI-003173",
- "CCI-001643"
+ "CCI-001310"
],
"nist": [
"SI-10"
],
"cwe": [
- "CWE-120",
- "CWE-20"
+ "CWE-20",
+ "CWE-120"
]
},
"refs": [],
@@ -56,8 +55,7 @@
{
"tags": {
"cci": [
- "CCI-003173",
- "CCI-001643"
+ "CCI-001310"
],
"nist": [
"SI-10"
@@ -102,8 +100,7 @@
{
"tags": {
"cci": [
- "CCI-003173",
- "CCI-001643"
+ "CCI-001310"
],
"nist": [
"SI-10"
@@ -138,8 +135,7 @@
{
"tags": {
"cci": [
- "CCI-003173",
- "CCI-001643"
+ "CCI-001310"
],
"nist": [
"SI-10"
@@ -204,8 +200,7 @@
{
"tags": {
"cci": [
- "CCI-003173",
- "CCI-001643"
+ "CCI-000213"
],
"nist": [
"AC-3"
@@ -250,8 +245,7 @@
{
"tags": {
"cci": [
- "CCI-003173",
- "CCI-001643"
+ "CCI-001310"
],
"nist": [
"SI-10"
@@ -311,8 +305,7 @@
{
"tags": {
"cci": [
- "CCI-003173",
- "CCI-001643"
+ "CCI-001310"
],
"nist": [
"SI-10"
@@ -387,8 +380,7 @@
{
"tags": {
"cci": [
- "CCI-003173",
- "CCI-001643"
+ "CCI-001310"
],
"nist": [
"SI-10"
@@ -423,15 +415,14 @@
{
"tags": {
"cci": [
- "CCI-003173",
- "CCI-001643"
+ "CCI-001310"
],
"nist": [
"SI-10"
],
"cwe": [
- "CWE-120",
- "CWE-20"
+ "CWE-20",
+ "CWE-120"
]
},
"refs": [],
@@ -480,8 +471,7 @@
{
"tags": {
"cci": [
- "CCI-003173",
- "CCI-001643"
+ "CCI-001310"
],
"nist": [
"SI-10"
@@ -516,8 +506,7 @@
{
"tags": {
"cci": [
- "CCI-003173",
- "CCI-001643"
+ "CCI-001310"
],
"nist": [
"SI-10"
@@ -552,8 +541,7 @@
{
"tags": {
"cci": [
- "CCI-003173",
- "CCI-001643"
+ "CCI-001310"
],
"nist": [
"SI-10"
@@ -588,8 +576,7 @@
{
"tags": {
"cci": [
- "CCI-003173",
- "CCI-001643"
+ "CCI-001310"
],
"nist": [
"SI-10"
@@ -634,15 +621,14 @@
{
"tags": {
"cci": [
- "CCI-003173",
- "CCI-001643"
+ "CCI-001310"
],
"nist": [
"SI-10"
],
"cwe": [
- "CWE-120",
- "CWE-20"
+ "CWE-20",
+ "CWE-120"
]
},
"refs": [],
@@ -671,15 +657,14 @@
{
"tags": {
"cci": [
- "CCI-003173",
- "CCI-001643"
+ "CCI-001310"
],
"nist": [
"SI-10"
],
"cwe": [
- "CWE-829",
- "CWE-20"
+ "CWE-20",
+ "CWE-829"
]
},
"refs": [],
@@ -703,8 +688,7 @@
{
"tags": {
"cci": [
- "CCI-003173",
- "CCI-001643"
+ "CCI-001310"
],
"nist": [
"SI-10"
@@ -755,8 +739,7 @@
{
"tags": {
"cci": [
- "CCI-003173",
- "CCI-001643"
+ "CCI-001310"
],
"nist": [
"SI-10"
@@ -811,8 +794,7 @@
{
"tags": {
"cci": [
- "CCI-003173",
- "CCI-001643"
+ "CCI-001090"
],
"nist": [
"SC-4"
@@ -847,8 +829,7 @@
{
"tags": {
"cci": [
- "CCI-003173",
- "CCI-001643"
+ "CCI-001310"
],
"nist": [
"SI-10"
@@ -883,8 +864,7 @@
{
"tags": {
"cci": [
- "CCI-003173",
- "CCI-001643"
+ "CCI-001310"
],
"nist": [
"SI-10"
@@ -919,8 +899,7 @@
{
"tags": {
"cci": [
- "CCI-003173",
- "CCI-001643"
+ "CCI-001310"
],
"nist": [
"SI-10"
@@ -953,7 +932,7 @@
]
}
],
- "sha256": "c5ef8179c102a4ffdf896f4b07be4b8ec34b0c6f83037aa9372d390ab11be340"
+ "sha256": "c931b56079dbdad9c16c730972f6a0d380109fc1d180b073b7b026fb72975907"
}
],
"passthrough": {
diff --git a/libs/hdf-converters/sample_jsons/sarif_mapper/sarif-hdf.json b/libs/hdf-converters/sample_jsons/sarif_mapper/sarif-hdf.json
index 6e0aa9e731..1a30f05397 100644
--- a/libs/hdf-converters/sample_jsons/sarif_mapper/sarif-hdf.json
+++ b/libs/hdf-converters/sample_jsons/sarif_mapper/sarif-hdf.json
@@ -1,10 +1,10 @@
{
"platform": {
"name": "Heimdall Tools",
- "release": "2.6.29",
+ "release": "2.12.6",
"target_id": "Static Analysis Results Interchange Format"
},
- "version": "2.6.29",
+ "version": "2.12.6",
"statistics": {},
"profiles": [
{
@@ -19,15 +19,14 @@
{
"tags": {
"cci": [
- "CCI-003173",
- "CCI-001643"
+ "CCI-001310"
],
"nist": [
"SI-10"
],
"cwe": [
- "CWE-120",
- "CWE-20"
+ "CWE-20",
+ "CWE-120"
]
},
"refs": [],
@@ -56,8 +55,7 @@
{
"tags": {
"cci": [
- "CCI-003173",
- "CCI-001643"
+ "CCI-001310"
],
"nist": [
"SI-10"
@@ -102,8 +100,7 @@
{
"tags": {
"cci": [
- "CCI-003173",
- "CCI-001643"
+ "CCI-001310"
],
"nist": [
"SI-10"
@@ -138,8 +135,7 @@
{
"tags": {
"cci": [
- "CCI-003173",
- "CCI-001643"
+ "CCI-001310"
],
"nist": [
"SI-10"
@@ -204,8 +200,7 @@
{
"tags": {
"cci": [
- "CCI-003173",
- "CCI-001643"
+ "CCI-000213"
],
"nist": [
"AC-3"
@@ -250,8 +245,7 @@
{
"tags": {
"cci": [
- "CCI-003173",
- "CCI-001643"
+ "CCI-001310"
],
"nist": [
"SI-10"
@@ -311,8 +305,7 @@
{
"tags": {
"cci": [
- "CCI-003173",
- "CCI-001643"
+ "CCI-001310"
],
"nist": [
"SI-10"
@@ -387,8 +380,7 @@
{
"tags": {
"cci": [
- "CCI-003173",
- "CCI-001643"
+ "CCI-001310"
],
"nist": [
"SI-10"
@@ -423,15 +415,14 @@
{
"tags": {
"cci": [
- "CCI-003173",
- "CCI-001643"
+ "CCI-001310"
],
"nist": [
"SI-10"
],
"cwe": [
- "CWE-120",
- "CWE-20"
+ "CWE-20",
+ "CWE-120"
]
},
"refs": [],
@@ -480,8 +471,7 @@
{
"tags": {
"cci": [
- "CCI-003173",
- "CCI-001643"
+ "CCI-001310"
],
"nist": [
"SI-10"
@@ -516,8 +506,7 @@
{
"tags": {
"cci": [
- "CCI-003173",
- "CCI-001643"
+ "CCI-001310"
],
"nist": [
"SI-10"
@@ -552,8 +541,7 @@
{
"tags": {
"cci": [
- "CCI-003173",
- "CCI-001643"
+ "CCI-001310"
],
"nist": [
"SI-10"
@@ -588,8 +576,7 @@
{
"tags": {
"cci": [
- "CCI-003173",
- "CCI-001643"
+ "CCI-001310"
],
"nist": [
"SI-10"
@@ -634,15 +621,14 @@
{
"tags": {
"cci": [
- "CCI-003173",
- "CCI-001643"
+ "CCI-001310"
],
"nist": [
"SI-10"
],
"cwe": [
- "CWE-120",
- "CWE-20"
+ "CWE-20",
+ "CWE-120"
]
},
"refs": [],
@@ -671,15 +657,14 @@
{
"tags": {
"cci": [
- "CCI-003173",
- "CCI-001643"
+ "CCI-001310"
],
"nist": [
"SI-10"
],
"cwe": [
- "CWE-829",
- "CWE-20"
+ "CWE-20",
+ "CWE-829"
]
},
"refs": [],
@@ -703,8 +688,7 @@
{
"tags": {
"cci": [
- "CCI-003173",
- "CCI-001643"
+ "CCI-001310"
],
"nist": [
"SI-10"
@@ -755,8 +739,7 @@
{
"tags": {
"cci": [
- "CCI-003173",
- "CCI-001643"
+ "CCI-001310"
],
"nist": [
"SI-10"
@@ -811,8 +794,7 @@
{
"tags": {
"cci": [
- "CCI-003173",
- "CCI-001643"
+ "CCI-001090"
],
"nist": [
"SC-4"
@@ -847,8 +829,7 @@
{
"tags": {
"cci": [
- "CCI-003173",
- "CCI-001643"
+ "CCI-001310"
],
"nist": [
"SI-10"
@@ -883,8 +864,7 @@
{
"tags": {
"cci": [
- "CCI-003173",
- "CCI-001643"
+ "CCI-001310"
],
"nist": [
"SI-10"
@@ -919,8 +899,7 @@
{
"tags": {
"cci": [
- "CCI-003173",
- "CCI-001643"
+ "CCI-001310"
],
"nist": [
"SI-10"
@@ -953,7 +932,7 @@
]
}
],
- "sha256": "c5ef8179c102a4ffdf896f4b07be4b8ec34b0c6f83037aa9372d390ab11be340"
+ "sha256": "c931b56079dbdad9c16c730972f6a0d380109fc1d180b073b7b026fb72975907"
}
],
"passthrough": {
diff --git a/libs/hdf-converters/src/sarif-mapper.ts b/libs/hdf-converters/src/sarif-mapper.ts
index c45188ae7e..23fdeb0fbd 100644
--- a/libs/hdf-converters/src/sarif-mapper.ts
+++ b/libs/hdf-converters/src/sarif-mapper.ts
@@ -16,12 +16,22 @@ const IMPACT_MAPPING: Map = new Map([
const MESSAGE_TEXT = 'message.text';
const CWE_NIST_MAPPING = new CweNistMapping();
-function extractCwe(text: string): string[] {
- let output = text.split('(').slice(-1)[0].slice(0, -2).split(', ');
- if (output.length === 1) {
- output = text.split('(').slice(-1)[0].slice(0, -2).split('!/');
+// CWE identifiers wherever they appear, in any of the forms producers emit:
+// a rule tag ('CWE-79: Improper Neutralization ...'), a bare token, or a
+// parenthesised list at the end of a message ('... (CWE-120, CWE-20).').
+const CWE_PATTERN = /CWE-(\d+)/gi;
+
+function extractCweIdentifiers(...sources: unknown[]): string[] {
+ const identifiers = new Set();
+ for (const source of sources) {
+ if (!_.isString(source)) {
+ continue;
+ }
+ for (const match of source.matchAll(CWE_PATTERN)) {
+ identifiers.add(`CWE-${match[1]}`);
+ }
}
- return output;
+ return [...identifiers];
}
function impactMapping(severity: unknown): number {
if (typeof severity === 'string' || typeof severity === 'number') {
@@ -37,17 +47,67 @@ function formatCodeDesc(input: unknown): string {
output.push(`COLUMN : ${_.get(input, 'region.startColumn')}`);
return output.join(' ');
}
-function nistTag(text: string): string[] {
- let identifiers = extractCwe(text);
- identifiers = identifiers.map((element) => element.split('-')[1]);
+function nistTag(cweIdentifiers: string[]): string[] {
return CWE_NIST_MAPPING.nistFilter(
- identifiers,
+ cweIdentifiers.map((identifier) => identifier.split('-')[1]),
DEFAULT_STATIC_CODE_ANALYSIS_NIST_TAGS
);
}
+// SARIF carries rule metadata on the rule object, not on the result: a result
+// only references its rule by id. Producers publish CWEs there in two places --
+// `properties.tags` (Semgrep, CodeQL) and `relationships[].target.id` against a
+// CWE taxonomy (the standards-blessed form) -- and a result's message may carry
+// none at all. Indexing every rule once lets a result resolve its own.
+function indexRuleCweIdentifiers(document: unknown): Map {
+ const index = new Map();
+ const runs: unknown = _.get(document, 'runs');
+ if (!Array.isArray(runs)) {
+ return index;
+ }
+ for (const run of runs) {
+ const extensions = _.get(run, 'tool.extensions') as unknown;
+ const components: unknown[] = [
+ _.get(run, 'tool.driver'),
+ ...(Array.isArray(extensions) ? extensions : [])
+ ];
+ for (const component of components) {
+ const rules: unknown = _.get(component, 'rules');
+ if (!Array.isArray(rules)) {
+ continue;
+ }
+ for (const rule of rules) {
+ const id: unknown = _.get(rule, 'id');
+ if (!_.isString(id)) {
+ continue;
+ }
+ const properties: unknown = _.get(rule, 'properties');
+ const ruleTags = _.get(properties, 'tags') as unknown;
+ const ruleCwe = _.get(properties, 'cwe') as unknown;
+ const relationships = _.get(rule, 'relationships') as unknown;
+ const identifiers = extractCweIdentifiers(
+ ...(Array.isArray(ruleTags) ? ruleTags : []),
+ ...(Array.isArray(ruleCwe) ? ruleCwe : [ruleCwe]),
+ ...(Array.isArray(relationships)
+ ? relationships.map((relationship) =>
+ _.get(relationship, 'target.id')
+ )
+ : [])
+ );
+ if (identifiers.length > 0) {
+ index.set(id, [
+ ...new Set([...(index.get(id) ?? []), ...identifiers])
+ ]);
+ }
+ }
+ }
+ }
+ return index;
+}
+
export class SarifMapper extends BaseConverter {
withRaw: boolean;
+ ruleCweIdentifiers: Map = new Map();
mappings: MappedTransform<
ExecJSON.Execution & {passthrough: unknown},
@@ -75,14 +135,14 @@ export class SarifMapper extends BaseConverter {
path: 'results',
key: 'id',
tags: {
- cci: {
- path: 'vulnerabilityClassifications',
- transformer: (data: string) => getCCIsForNISTTags(nistTag(data))
- },
- nist: {path: MESSAGE_TEXT, transformer: nistTag},
- cwe: {
- path: MESSAGE_TEXT,
- transformer: extractCwe
+ // All three derive from one CWE resolution. Previously each was
+ // computed independently and `cci` read a path SARIF does not
+ // define, so it always fell through to the default tags' CCIs --
+ // which disagreed with the `nist` tag on the same control.
+ transformer: (result: unknown): Record => {
+ const cwe = this.resolveCweIdentifiers(result);
+ const nist = nistTag(cwe);
+ return {cci: getCCIsForNISTTags(nist), nist, cwe};
}
},
refs: [],
@@ -168,8 +228,23 @@ export class SarifMapper extends BaseConverter {
}
}
};
+ // Prefers the rule's own metadata and falls back to scraping the result
+ // message. The fallback is what flawfinder-style producers rely on -- their
+ // rules carry no properties and the CWEs appear only in the message text.
+ resolveCweIdentifiers(result: unknown): string[] {
+ const ruleId = _.get(result, 'ruleId');
+ if (_.isString(ruleId)) {
+ const fromRule = this.ruleCweIdentifiers.get(ruleId);
+ if (fromRule !== undefined && fromRule.length > 0) {
+ return fromRule;
+ }
+ }
+ return extractCweIdentifiers(_.get(result, MESSAGE_TEXT));
+ }
+
constructor(sarifJson: string, withRaw = false) {
super(JSON.parse(sarifJson));
this.withRaw = withRaw;
+ this.ruleCweIdentifiers = indexRuleCweIdentifiers(this.data);
}
}
diff --git a/libs/hdf-converters/test/mappers/forward/sarif_mapper.spec.ts b/libs/hdf-converters/test/mappers/forward/sarif_mapper.spec.ts
index 43eed17967..9679f55207 100644
--- a/libs/hdf-converters/test/mappers/forward/sarif_mapper.spec.ts
+++ b/libs/hdf-converters/test/mappers/forward/sarif_mapper.spec.ts
@@ -54,3 +54,94 @@ describe('sarif_mapper_withraw', () => {
);
});
});
+
+describe('sarif_mapper CWE resolution', () => {
+ function convert(sample: string, withRaw = false) {
+ return new SarifMapper(
+ fs.readFileSync(
+ `sample_jsons/sarif_mapper/sample_input_report/${sample}`,
+ {encoding: 'utf-8'}
+ ),
+ withRaw
+ ).toHdf();
+ }
+
+ it('reads CWEs from rule relationships against a CWE taxonomy', () => {
+ // flawfinder publishes them in rules[].relationships[].target.id, the
+ // standards-blessed location, as well as in the message text.
+ const control = convert('sarif_input.sarif').profiles[0].controls.find(
+ (candidate) => candidate.id === 'FF1059'
+ );
+ expect(control?.tags.cwe).toEqual(
+ expect.arrayContaining(['CWE-20', 'CWE-829'])
+ );
+ });
+
+ it('reads CWEs from rule properties.tags', () => {
+ // Semgrep publishes them as prose entries in the rule's tag list, and puts
+ // nothing CWE-shaped in the result message.
+ const control = convert('semgrep_sarif.sarif').profiles[0].controls[0];
+ expect(control.tags.cwe).toEqual(['CWE-939']);
+ });
+
+ it('falls back to the result message when the rule carries no CWE', () => {
+ const sarif = {
+ version: '2.1.0',
+ runs: [
+ {
+ tool: {driver: {name: 'Bare', rules: [{id: 'R1'}]}},
+ results: [
+ {
+ ruleId: 'R1',
+ level: 'error',
+ message: {text: 'Something bad: happened (CWE-79).'},
+ locations: []
+ }
+ ]
+ }
+ ]
+ };
+ const control = new SarifMapper(JSON.stringify(sarif)).toHdf().profiles[0]
+ .controls[0];
+ expect(control.tags.cwe).toEqual(['CWE-79']);
+ });
+
+ it('emits no CWE tags when neither the rule nor the message carries one', () => {
+ const sarif = {
+ version: '2.1.0',
+ runs: [
+ {
+ tool: {driver: {name: 'Bare', rules: [{id: 'R1'}]}},
+ results: [
+ {
+ ruleId: 'R1',
+ level: 'note',
+ message: {text: 'Prose with no identifier in it at all.'},
+ locations: []
+ }
+ ]
+ }
+ ]
+ };
+ const control = new SarifMapper(JSON.stringify(sarif)).toHdf().profiles[0]
+ .controls[0];
+ // Previously the scrape returned a fragment of the prose as a "CWE".
+ expect(control.tags.cwe).toEqual([]);
+ // With nothing to map, the documented static-analysis defaults apply.
+ expect(control.tags.nist).toEqual(['SA-11', 'RA-5']);
+ });
+
+ it('derives cci from the nist tags on the same control', () => {
+ for (const control of convert('sarif_input.sarif').profiles[0].controls) {
+ const nist = control.tags.nist as string[];
+ const cci = control.tags.cci as string[];
+ expect(Array.isArray(cci)).toBe(true);
+ // SI-10 maps to CCI-001310; the previous implementation emitted the
+ // default tags' CCIs regardless of what nist actually resolved to.
+ if (nist.includes('SI-10')) {
+ expect(cci).toContain('CCI-001310');
+ expect(cci).not.toContain('CCI-003173');
+ }
+ }
+ });
+});