in packages/cdk-graph-plugin-threat-composer/src/model-generator/threat-model-generator.ts [66:240]
public generate(
results: ExtendedNagResult[],
options?: ThreatModelGeneratorOptions
): ThreatComposerModel {
// Summarise nag results into rules and the counts of resources compliant/non-compliant
const allApplicableRules = new Set<string>();
const compliance: { [nagRule: string]: ComplianceFindings } = {};
const suppressionReasons: { [nagRule: string]: string[] } = {};
for (const result of results) {
const rule = result.ruleOriginalName;
if (!compliance[rule]) {
compliance[rule] = {
compliantResources: new Set(),
nonCompliantResources: new Set(),
};
}
// Add to the set of all applicable rules so long as the rule is applicable
if (result.compliance !== NagResultCompliance.NOT_APPLICABLE) {
allApplicableRules.add(rule);
}
if (
[
NagResultCompliance.NON_COMPLIANT,
NagResultCompliance.ERROR,
NagResultCompliance.NON_COMPLIANT_SUPPRESSED,
NagResultCompliance.ERROR_SUPPRESSED,
].includes(result.compliance)
) {
// Add the resource path to the compliance set
compliance[rule].nonCompliantResources.add(result.resource.node.path);
if (result.suppressionReason) {
suppressionReasons[rule] = [
...(suppressionReasons[rule] ?? []),
result.suppressionReason,
];
}
} else if (result.compliance === NagResultCompliance.COMPLIANT) {
// Resource is compliant
compliance[rule].compliantResources.add(result.resource.node.path);
}
}
const mitigationsById = Object.fromEntries(
BASE_THREAT_MODEL.mitigations.map((m) => [m.id, m])
);
const threatIdToMitigationIds = BASE_THREAT_MODEL.mitigationLinks.reduce(
(byId, m) => ({
...byId,
[m.linkedId]: [...(byId[m.linkedId] ?? []), m.mitigationId],
}),
{} as { [threatId: string]: string[] }
);
// Get applicable threats - ie threats where there is a mitigation which is a CDK nag rule that is applicable to this project
const threats: ThreatComposerThreat[] = orderBy(
BASE_THREAT_MODEL.threats,
"numericId"
)
.filter((threat) => {
const mitigationIds = threatIdToMitigationIds[threat.id] ?? [];
return mitigationIds.find((id) => {
const mitigationRule = this.getRuleFromMitigation(
mitigationsById[id]
);
return mitigationRule && allApplicableRules.has(mitigationRule);
});
})
.map((t, i) => ({
...t,
// Re-map numeric ids and display order
numericId: i + 1,
displayOrder: i + 1,
}));
// Get applicable mitigations
const mitigations = (
orderBy(BASE_THREAT_MODEL.mitigations, "numericId")
.map((m) => {
const mitigationRule = this.getRuleFromMitigation(
mitigationsById[m.id]
);
if (mitigationRule && compliance[mitigationRule]) {
const { compliantResources, nonCompliantResources } =
compliance[mitigationRule];
const suppressions = suppressionReasons[mitigationRule];
const compliant = compliantResources.size;
const nonCompliant = nonCompliantResources.size;
// We can't really warrant adding a mitigation when 0 resources are compliant and there are no suppression reasons
if (
compliant === 0 &&
(!suppressions || suppressions.length === 0)
) {
return undefined;
}
const suppressionComment = suppressions
? `\n\n__Suppression Reasons__:\n${this.prettySuppressions(
suppressions
)
.map((reason) => `* ${reason}`)
.join("\n")}`
: "";
let comment = `${compliant} of ${
compliant + nonCompliant
} Resources Compliant.${suppressionComment}`;
// Threat composer limits comments to 1000 chars
if (comment.length > 1000) {
comment = comment.slice(0, 997) + "...";
}
return {
...m,
metadata: [
// TODO: Consider appending to existing comments rather than overriding
{
key: "Comments",
value: comment,
},
],
};
}
return undefined;
})
.filter((x) => x) as ThreatComposerMitigation[]
).map((m, i) => ({
...m,
// Re-map numeric ids and display order
numericId: i + 1,
displayOrder: i + 1,
}));
// Include only mitigation links where we have threats and mitigations
const projectThreatIds = new Set(threats.map(({ id }) => id));
const projectMitigationIds = new Set(mitigations.map(({ id }) => id));
const mitigationLinks = BASE_THREAT_MODEL.mitigationLinks.filter(
(link) =>
projectThreatIds.has(link.linkedId) &&
projectMitigationIds.has(link.mitigationId)
);
const threatModel = {
...BASE_THREAT_MODEL,
threats,
mitigations,
mitigationLinks,
architecture: {
...BASE_THREAT_MODEL.architecture,
image: options?.architectureImageDataUri ?? "",
},
applicationInfo: {
...BASE_THREAT_MODEL.applicationInfo,
name:
options?.applicationDetails?.name ??
BASE_THREAT_MODEL.applicationInfo.name,
description:
options?.applicationDetails?.description ??
BASE_THREAT_MODEL.applicationInfo.description,
},
};
// jest interprets the "import * as" import differently, so we remove this to ensure the snapshot
// is more realistic
if ("default" in threatModel) {
delete threatModel.default;
}
return threatModel;
}