-
Notifications
You must be signed in to change notification settings - Fork 72
Expand file tree
/
Copy pathSecurityMapDetails.tsx
More file actions
134 lines (122 loc) · 3.62 KB
/
Copy pathSecurityMapDetails.tsx
File metadata and controls
134 lines (122 loc) · 3.62 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
import { Link } from "vocs";
import {
NODE_TYPE_LABELS,
type AssessmentState,
type NodeType,
type SecurityMapNode,
} from "./types";
import { gapReasons, relatedByType, type GraphIndex } from "./graphIndex";
import { SecurityMapAssessment } from "./SecurityMapAssessment";
import type { AssessmentDocument } from "./assessment";
const MAP_REL_TYPES: NodeType[] = ["attack-surface", "threat", "control"];
const COUNT_TYPES: NodeType[] = [
"attack-surface",
"threat",
"control",
"incident",
"guidance",
"response",
];
function pageLinks(index: GraphIndex, id: string): SecurityMapNode[] {
const seen: Record<string, true> = Object.create(null);
const pages: SecurityMapNode[] = [];
const add = (node: SecurityMapNode | undefined) => {
if (!node?.href || seen[node.id]) return;
seen[node.id] = true;
pages.push(node);
};
add(index.nodesById[id]);
for (const edge of index.outgoing[id] || []) {
if (edge.type === "documented-by" || edge.type === "evaluated-by") add(index.nodesById[edge.target]);
}
return pages;
}
export function SecurityMapDetails({
index,
node,
onSelect,
onClose,
assessment,
onAssess,
}: {
index: GraphIndex;
node: SecurityMapNode | null;
onSelect: (id: string) => void;
onClose: () => void;
assessment: AssessmentDocument;
onAssess: (id: string, state: AssessmentState) => void;
}) {
if (!node) {
return (
<aside className="sm-details">
<h2>Selection</h2>
<p className="sm-empty">Click a field. Related fields in the other columns light up.</p>
</aside>
);
}
const related = relatedByType(index, node.id, 2);
const assessed = assessment.controls[node.id];
const reasons = gapReasons(index, node.id);
const pages = pageLinks(index, node.id);
return (
<aside className="sm-details" aria-labelledby="sm-detail-title">
<div className="sm-details-head">
<p className="sm-kicker">{NODE_TYPE_LABELS[node.type]}</p>
<button type="button" className="sm-btn" onClick={onClose}>
Clear
</button>
</div>
<h2 id="sm-detail-title">{node.title}</h2>
<p>{node.summary}</p>
{pages.length ? (
<ul className="sm-rel">
{pages.map((page) => (
<li key={page.id}>
<Link className="sm-page-link" to={page.href as string}>
{page.id === node.id ? "Open framework page" : page.title}
</Link>
</li>
))}
</ul>
) : null}
<ul className="sm-stats">
{COUNT_TYPES.map((type) => (
<li key={type}>
<strong>{related[type].length}</strong>
<span>{NODE_TYPE_LABELS[type]}</span>
</li>
))}
</ul>
{reasons.length ? (
<ul className="sm-gaps">
{reasons.map((reason) => (
<li key={reason}>{reason}</li>
))}
</ul>
) : null}
{MAP_REL_TYPES.map((type) =>
related[type].length ? (
<div key={type}>
<h3>{NODE_TYPE_LABELS[type]}</h3>
<ul className="sm-rel">
{related[type].map((other) => (
<li key={other.id}>
<button type="button" onClick={() => onSelect(other.id)}>
{other.title}
</button>
</li>
))}
</ul>
</div>
) : null,
)}
{node.type === "control" && node.assessmentEligible !== false ? (
<SecurityMapAssessment
controlId={node.id}
state={assessed?.state || "not-assessed"}
onAssess={onAssess}
/>
) : null}
</aside>
);
}