capagap 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- capagap/__init__.py +9 -0
- capagap/__main__.py +3 -0
- capagap/analysis.py +265 -0
- capagap/assets/report.css +317 -0
- capagap/assets/report.js +215 -0
- capagap/cli.py +465 -0
- capagap/handoff.py +376 -0
- capagap/hotspots.py +45 -0
- capagap/html_report.py +717 -0
- capagap/importers/binary_ninja.py.tmpl +114 -0
- capagap/importers/ghidra.py.tmpl +88 -0
- capagap/importers/ida.py.tmpl +91 -0
- capagap/io.py +223 -0
- capagap/manifest.py +376 -0
- capagap/matrix.py +256 -0
- capagap/models.py +361 -0
- capagap/render.py +561 -0
- capagap/scoring.py +94 -0
- capagap/triage.py +254 -0
- capagap-0.1.0.dist-info/METADATA +145 -0
- capagap-0.1.0.dist-info/RECORD +25 -0
- capagap-0.1.0.dist-info/WHEEL +5 -0
- capagap-0.1.0.dist-info/entry_points.txt +2 -0
- capagap-0.1.0.dist-info/licenses/LICENSE +21 -0
- capagap-0.1.0.dist-info/top_level.txt +1 -0
capagap/__init__.py
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
"""CapaGap: evidence-aware comparison of static and dynamic capa results."""
|
|
2
|
+
|
|
3
|
+
__version__ = "0.1.0"
|
|
4
|
+
|
|
5
|
+
from capagap.analysis import compare_documents
|
|
6
|
+
from capagap.io import load_document
|
|
7
|
+
from capagap.matrix import compare_matrix
|
|
8
|
+
|
|
9
|
+
__all__ = ["compare_documents", "compare_matrix", "load_document"]
|
capagap/__main__.py
ADDED
capagap/analysis.py
ADDED
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
"""Core comparison engine."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Iterable
|
|
6
|
+
|
|
7
|
+
from capagap.hotspots import build_evidence_hotspots
|
|
8
|
+
from capagap.manifest import RuleManifest
|
|
9
|
+
from capagap.models import (
|
|
10
|
+
CapaDocument,
|
|
11
|
+
Comparison,
|
|
12
|
+
ComparisonSummary,
|
|
13
|
+
Finding,
|
|
14
|
+
RuleRecord,
|
|
15
|
+
)
|
|
16
|
+
from capagap.scoring import priority_label, score_rule, suggested_action
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class ComparisonError(ValueError):
|
|
20
|
+
"""Raised when two documents cannot be compared responsibly."""
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _major(version: str) -> str | None:
|
|
24
|
+
head = version.split(".", 1)[0]
|
|
25
|
+
return head if head.isdigit() else None
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _filter_rules(
|
|
29
|
+
document: CapaDocument, include_library: bool
|
|
30
|
+
) -> dict[str, RuleRecord]:
|
|
31
|
+
return {
|
|
32
|
+
name: rule
|
|
33
|
+
for name, rule in document.rules.items()
|
|
34
|
+
if include_library or not rule.library
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _finding(
|
|
39
|
+
status: str, rule: RuleRecord, *, evasion_context: bool = False
|
|
40
|
+
) -> Finding:
|
|
41
|
+
if status == "unobserved":
|
|
42
|
+
score, reasons = score_rule(rule, evasion_context=evasion_context)
|
|
43
|
+
return Finding(
|
|
44
|
+
status=status,
|
|
45
|
+
rule=rule,
|
|
46
|
+
priority=score,
|
|
47
|
+
priority_label=priority_label(score),
|
|
48
|
+
reasons=reasons,
|
|
49
|
+
action=suggested_action(rule),
|
|
50
|
+
)
|
|
51
|
+
return Finding(status=status, rule=rule, priority=0, priority_label="informational")
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _sort_findings(findings: Iterable[Finding]) -> tuple[Finding, ...]:
|
|
55
|
+
return tuple(
|
|
56
|
+
sorted(findings, key=lambda item: (-item.priority, item.rule.name.lower()))
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def compare_documents(
|
|
61
|
+
static: CapaDocument,
|
|
62
|
+
dynamic: CapaDocument,
|
|
63
|
+
*,
|
|
64
|
+
allow_mismatch: bool = False,
|
|
65
|
+
include_library: bool = False,
|
|
66
|
+
ruleset_manifest: RuleManifest | None = None,
|
|
67
|
+
) -> Comparison:
|
|
68
|
+
"""Compare static potential with behavior observed in one dynamic run."""
|
|
69
|
+
|
|
70
|
+
if static.flavor != "static":
|
|
71
|
+
raise ComparisonError(f"first input must be static, got {static.flavor}")
|
|
72
|
+
if dynamic.flavor != "dynamic":
|
|
73
|
+
raise ComparisonError(f"second input must be dynamic, got {dynamic.flavor}")
|
|
74
|
+
|
|
75
|
+
warnings: list[str] = []
|
|
76
|
+
confidence = "high"
|
|
77
|
+
hashes_present = bool(static.sample_sha256 and dynamic.sample_sha256)
|
|
78
|
+
hashes_match = hashes_present and static.sample_sha256 == dynamic.sample_sha256
|
|
79
|
+
if hashes_present and not hashes_match:
|
|
80
|
+
message = (
|
|
81
|
+
"sample SHA-256 values differ; the reports may describe different samples"
|
|
82
|
+
)
|
|
83
|
+
if not allow_mismatch:
|
|
84
|
+
raise ComparisonError(
|
|
85
|
+
message + " (use --allow-mismatch only when this is intentional)"
|
|
86
|
+
)
|
|
87
|
+
warnings.append(message)
|
|
88
|
+
confidence = "low"
|
|
89
|
+
elif not hashes_present:
|
|
90
|
+
warnings.append(
|
|
91
|
+
"one or both reports lack a sample SHA-256; sample identity is unverified"
|
|
92
|
+
)
|
|
93
|
+
confidence = "medium"
|
|
94
|
+
|
|
95
|
+
if static.os != dynamic.os:
|
|
96
|
+
warnings.append(f"OS differs: static={static.os}, dynamic={dynamic.os}")
|
|
97
|
+
confidence = (
|
|
98
|
+
"low"
|
|
99
|
+
if confidence == "medium"
|
|
100
|
+
else "medium"
|
|
101
|
+
if confidence == "high"
|
|
102
|
+
else confidence
|
|
103
|
+
)
|
|
104
|
+
if static.arch != dynamic.arch:
|
|
105
|
+
warnings.append(
|
|
106
|
+
f"architecture differs: static={static.arch}, dynamic={dynamic.arch}"
|
|
107
|
+
)
|
|
108
|
+
confidence = (
|
|
109
|
+
"low"
|
|
110
|
+
if confidence == "medium"
|
|
111
|
+
else "medium"
|
|
112
|
+
if confidence == "high"
|
|
113
|
+
else confidence
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
static_major = _major(static.capa_version)
|
|
117
|
+
dynamic_major = _major(dynamic.capa_version)
|
|
118
|
+
if static_major and dynamic_major and static_major != dynamic_major:
|
|
119
|
+
warnings.append(
|
|
120
|
+
f"capa major versions differ: static={static.capa_version}, dynamic={dynamic.capa_version}"
|
|
121
|
+
)
|
|
122
|
+
confidence = (
|
|
123
|
+
"low"
|
|
124
|
+
if confidence == "medium"
|
|
125
|
+
else "medium"
|
|
126
|
+
if confidence == "high"
|
|
127
|
+
else confidence
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
static_rules = _filter_rules(static, include_library)
|
|
131
|
+
dynamic_rules = _filter_rules(dynamic, include_library)
|
|
132
|
+
raw_comparable = {
|
|
133
|
+
name: rule for name, rule in static_rules.items() if rule.dynamically_comparable
|
|
134
|
+
}
|
|
135
|
+
static_only_rules = {
|
|
136
|
+
name: rule
|
|
137
|
+
for name, rule in static_rules.items()
|
|
138
|
+
if not rule.dynamically_comparable
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
ruleset_unverified: tuple[Finding, ...] = ()
|
|
142
|
+
comparable = raw_comparable
|
|
143
|
+
if ruleset_manifest is not None:
|
|
144
|
+
for name, rule in dynamic_rules.items():
|
|
145
|
+
entry = ruleset_manifest.rules.get(name)
|
|
146
|
+
if entry is None:
|
|
147
|
+
raise ComparisonError(
|
|
148
|
+
f"dynamic result rule {name!r} is absent from ruleset manifest"
|
|
149
|
+
)
|
|
150
|
+
if entry.source_digest != rule.source_digest:
|
|
151
|
+
raise ComparisonError(
|
|
152
|
+
f"dynamic result rule {name!r} does not match its manifest source digest"
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
verified = {
|
|
156
|
+
name: rule
|
|
157
|
+
for name, rule in raw_comparable.items()
|
|
158
|
+
if (
|
|
159
|
+
name in ruleset_manifest.rules
|
|
160
|
+
and ruleset_manifest.rules[name].source_digest == rule.source_digest
|
|
161
|
+
)
|
|
162
|
+
}
|
|
163
|
+
unverified_names = raw_comparable.keys() - verified.keys()
|
|
164
|
+
ruleset_unverified = _sort_findings(
|
|
165
|
+
Finding(
|
|
166
|
+
status="ruleset-unverified",
|
|
167
|
+
rule=raw_comparable[name],
|
|
168
|
+
priority=0,
|
|
169
|
+
priority_label="informational",
|
|
170
|
+
reasons=(
|
|
171
|
+
"rule is absent from the supplied manifest"
|
|
172
|
+
if name not in ruleset_manifest.rules
|
|
173
|
+
else "static rule source differs from the supplied manifest",
|
|
174
|
+
"excluded from verified coverage",
|
|
175
|
+
),
|
|
176
|
+
action="Regenerate both capa results and the manifest from the same ruleset revision.",
|
|
177
|
+
)
|
|
178
|
+
for name in unverified_names
|
|
179
|
+
)
|
|
180
|
+
comparable = verified
|
|
181
|
+
if ruleset_unverified:
|
|
182
|
+
warnings.append(
|
|
183
|
+
f"{len(ruleset_unverified)} comparable static rule(s) could not be verified against the supplied ruleset manifest and were excluded from coverage"
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
observed_names = comparable.keys() & dynamic_rules.keys()
|
|
187
|
+
unobserved_names = comparable.keys() - dynamic_rules.keys()
|
|
188
|
+
dynamic_only_names = dynamic_rules.keys() - static_rules.keys()
|
|
189
|
+
|
|
190
|
+
gate_signals = tuple(
|
|
191
|
+
sorted(
|
|
192
|
+
static_rules[name].name
|
|
193
|
+
for name in observed_names
|
|
194
|
+
if "anti-analysis" in static_rules[name].namespace.lower()
|
|
195
|
+
)
|
|
196
|
+
)
|
|
197
|
+
evasion_context = bool(gate_signals)
|
|
198
|
+
|
|
199
|
+
observed = _sort_findings(
|
|
200
|
+
_finding("observed", static_rules[name]) for name in observed_names
|
|
201
|
+
)
|
|
202
|
+
unobserved = _sort_findings(
|
|
203
|
+
_finding("unobserved", static_rules[name], evasion_context=evasion_context)
|
|
204
|
+
for name in unobserved_names
|
|
205
|
+
)
|
|
206
|
+
static_only = _sort_findings(
|
|
207
|
+
_finding("static-only", static_only_rules[name]) for name in static_only_rules
|
|
208
|
+
)
|
|
209
|
+
dynamic_only = _sort_findings(
|
|
210
|
+
_finding("dynamic-only", dynamic_rules[name]) for name in dynamic_only_names
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
source_drift = tuple(
|
|
214
|
+
sorted(
|
|
215
|
+
name
|
|
216
|
+
for name in static_rules.keys() & dynamic_rules.keys()
|
|
217
|
+
if static_rules[name].source_digest != dynamic_rules[name].source_digest
|
|
218
|
+
)
|
|
219
|
+
)
|
|
220
|
+
if source_drift:
|
|
221
|
+
warnings.append(
|
|
222
|
+
f"{len(source_drift)} shared rule(s) have different source text between reports; rule-set drift may affect results"
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
summary = ComparisonSummary(
|
|
226
|
+
static_rules=len(static_rules),
|
|
227
|
+
dynamic_rules=len(dynamic_rules),
|
|
228
|
+
comparable_static_rules=len(comparable),
|
|
229
|
+
observed_rules=len(observed),
|
|
230
|
+
unobserved_rules=len(unobserved),
|
|
231
|
+
static_only_rules=len(static_only),
|
|
232
|
+
dynamic_only_rules=len(dynamic_only),
|
|
233
|
+
ruleset_unverified_rules=len(ruleset_unverified),
|
|
234
|
+
)
|
|
235
|
+
metadata = {
|
|
236
|
+
"include_library_rules": include_library,
|
|
237
|
+
"sample_hashes_match": hashes_match,
|
|
238
|
+
"static_extractor": static.extractor,
|
|
239
|
+
"dynamic_extractor": dynamic.extractor,
|
|
240
|
+
}
|
|
241
|
+
if ruleset_manifest is not None:
|
|
242
|
+
metadata["ruleset_manifest"] = {
|
|
243
|
+
"source_name": ruleset_manifest.source_name,
|
|
244
|
+
"fingerprint": ruleset_manifest.fingerprint,
|
|
245
|
+
"path": ruleset_manifest.path.name,
|
|
246
|
+
"verified_comparable_rules": len(comparable),
|
|
247
|
+
"excluded_unverified_rules": len(ruleset_unverified),
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
return Comparison(
|
|
251
|
+
static=static,
|
|
252
|
+
dynamic=dynamic,
|
|
253
|
+
confidence=confidence,
|
|
254
|
+
summary=summary,
|
|
255
|
+
unobserved=unobserved,
|
|
256
|
+
observed=observed,
|
|
257
|
+
static_only=static_only,
|
|
258
|
+
dynamic_only=dynamic_only,
|
|
259
|
+
ruleset_unverified=ruleset_unverified,
|
|
260
|
+
evidence_hotspots=build_evidence_hotspots(unobserved, static.base_address),
|
|
261
|
+
gate_signals=gate_signals,
|
|
262
|
+
warnings=tuple(warnings),
|
|
263
|
+
source_drift=source_drift,
|
|
264
|
+
metadata=metadata,
|
|
265
|
+
)
|
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
:root {
|
|
2
|
+
color-scheme: light;
|
|
3
|
+
--surface: #fff;
|
|
4
|
+
--subtle: #f6f7f9;
|
|
5
|
+
--selected: #edf3fb;
|
|
6
|
+
--line: #d7dde5;
|
|
7
|
+
--line-strong: #9aafc8;
|
|
8
|
+
--ink: #1e2733;
|
|
9
|
+
--muted: #566170;
|
|
10
|
+
--accent: #1b579f;
|
|
11
|
+
--accent-hover: #134579;
|
|
12
|
+
--observed: #227342;
|
|
13
|
+
--warning: #8a590d;
|
|
14
|
+
--focus: #2867b5;
|
|
15
|
+
--radius: 4px;
|
|
16
|
+
font: 16px/1.5 "Segoe UI", -apple-system, BlinkMacSystemFont, sans-serif;
|
|
17
|
+
}
|
|
18
|
+
* { box-sizing: border-box; }
|
|
19
|
+
body { margin: 0; background: var(--surface); color: var(--ink); }
|
|
20
|
+
::selection { background: #d8e7fa; color: var(--ink); }
|
|
21
|
+
button, input, select, textarea { font: inherit; color: inherit; }
|
|
22
|
+
button, summary { cursor: pointer; }
|
|
23
|
+
button { background: none; }
|
|
24
|
+
button:disabled { cursor: default; opacity: .5; }
|
|
25
|
+
button, a, input, select, summary { transition: background-color 150ms ease-out, color 150ms ease-out, border-color 150ms ease-out; }
|
|
26
|
+
a { color: var(--accent); text-underline-offset: 3px; }
|
|
27
|
+
a:hover { color: var(--accent-hover); }
|
|
28
|
+
:focus-visible { outline: 2px solid var(--focus); outline-offset: 3px; }
|
|
29
|
+
input, textarea { caret-color: var(--accent); }
|
|
30
|
+
[hidden] { display: none !important; }
|
|
31
|
+
svg { flex-shrink: 0; }
|
|
32
|
+
.icon { width: 20px; height: 20px; vertical-align: middle; }
|
|
33
|
+
code, .score { font-family: ui-monospace, "Cascadia Code", "SFMono-Regular", Consolas, monospace; font-size: .93em; font-variant-numeric: tabular-nums; }
|
|
34
|
+
h1, h2, h3, p { margin: 0; }
|
|
35
|
+
h2 { font-size: 18px; font-weight: 600; }
|
|
36
|
+
h3 { font-size: 16px; font-weight: 600; margin-bottom: 8px; }
|
|
37
|
+
h3 .muted { font-weight: 400; }
|
|
38
|
+
.muted { color: var(--muted); }
|
|
39
|
+
.sr-only { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0,0,0,0); white-space: nowrap; border: 0; }
|
|
40
|
+
.print-only { display: none; }
|
|
41
|
+
.skip-link { position: fixed; top: -80px; left: 20px; z-index: 10; padding: 10px 16px; background: var(--surface); border: 1px solid var(--accent); }
|
|
42
|
+
.skip-link:focus { top: 10px; }
|
|
43
|
+
.appbar { min-height: 68px; padding: 14px 22px; display: flex; align-items: center; justify-content: space-between; gap: 20px; border-bottom: 1px solid var(--line); }
|
|
44
|
+
.brand { display: flex; align-items: center; gap: 24px; min-width: 0; }
|
|
45
|
+
.wordmark { flex-shrink: 0; font-size: 30px; font-weight: 600; letter-spacing: -.03em; line-height: 1.2; }
|
|
46
|
+
.wordmark > span { color: var(--accent); }
|
|
47
|
+
.sample-name { font-size: 18px; padding-left: 22px; border-left: 1px solid var(--line); overflow-wrap: anywhere; }
|
|
48
|
+
.button { display: inline-flex; align-items: center; justify-content: center; gap: 9px; min-height: 38px; padding: 7px 13px; border: 1px solid var(--line); border-radius: var(--radius); background: var(--surface); font-size: 15px; }
|
|
49
|
+
.button:hover { background: var(--subtle); border-color: var(--line-strong); }
|
|
50
|
+
.appbar > .button { flex-shrink: 0; }
|
|
51
|
+
.sample-strip { min-height: 70px; padding: 18px 28px; border-bottom: 1px solid var(--line); display: flex; align-items: center; justify-content: space-between; gap: 16px 28px; }
|
|
52
|
+
.sample-metadata, .coverage-summary { display: flex; align-items: center; gap: 18px; flex-wrap: wrap; }
|
|
53
|
+
.sample-metadata { min-width: 0; }
|
|
54
|
+
.sample-metadata > span, .coverage-summary > span, .coverage-summary > a { padding-right: 18px; border-right: 1px solid var(--line); }
|
|
55
|
+
.sample-metadata > :last-child, .coverage-summary > :last-child { padding-right: 0; border: 0; }
|
|
56
|
+
.sample-metadata span { overflow-wrap: anywhere; }
|
|
57
|
+
.sample-metadata small { margin-right: 8px; color: var(--muted); font-size: 13px; }
|
|
58
|
+
.example-label { color: var(--accent); font-weight: 500; }
|
|
59
|
+
.coverage-summary { font-variant-numeric: tabular-nums; }
|
|
60
|
+
.coverage-summary strong { font-weight: 600; }
|
|
61
|
+
.coverage-summary > a { text-decoration: none; color: var(--ink); }
|
|
62
|
+
.coverage-summary > a:hover { color: var(--accent); text-decoration: underline; }
|
|
63
|
+
.coverage-summary .warning-link { color: var(--warning); display: inline-flex; gap: 6px; align-items: center; }
|
|
64
|
+
.workspace-toolbar { min-height: 70px; margin: 0 22px; display: flex; align-items: center; justify-content: space-between; gap: 16px; border-bottom: 1px solid var(--line); }
|
|
65
|
+
.workspace-toolbar nav { display: flex; align-self: stretch; align-items: stretch; gap: 16px; }
|
|
66
|
+
.workspace-toolbar nav a { display: flex; align-items: center; padding: 16px 10px; border-bottom: 2px solid transparent; text-decoration: none; color: var(--ink); }
|
|
67
|
+
.workspace-toolbar nav a:hover { background: var(--subtle); }
|
|
68
|
+
.workspace-toolbar nav a.active { color: var(--accent); border-color: var(--accent); font-weight: 500; }
|
|
69
|
+
.filters { display: flex; align-items: center; gap: 14px; }
|
|
70
|
+
.search-field { display: flex; align-items: center; gap: 10px; width: 320px; min-height: 40px; border: 1px solid var(--line); border-radius: var(--radius); padding: 0 12px; }
|
|
71
|
+
.search-field > .icon { color: var(--muted); width: 18px; height: 18px; }
|
|
72
|
+
.search-field:focus-within { outline: 2px solid var(--focus); outline-offset: 2px; }
|
|
73
|
+
.search-field input { width: 100%; min-width: 0; padding: 8px 0; border: 0; background: transparent; outline: none; }
|
|
74
|
+
input::placeholder { color: var(--muted); opacity: 1; }
|
|
75
|
+
.state-filter select { min-height: 40px; width: 180px; padding: 7px 32px 7px 12px; border: 1px solid var(--line); border-radius: var(--radius); background: var(--surface); }
|
|
76
|
+
.matrix-section { margin: 0 22px; scroll-margin-top: 18px; }
|
|
77
|
+
.matrix-section:focus { outline: none; }
|
|
78
|
+
.groupbar { display: flex; flex-wrap: wrap; align-items: center; gap: 14px; min-height: 74px; padding: 16px 0; }
|
|
79
|
+
.group-button { display: inline-flex; align-items: center; gap: 20px; min-height: 39px; padding: 7px 16px; border: 1px solid var(--line); border-radius: var(--radius); font-size: 15px; }
|
|
80
|
+
.group-button span { font-variant-numeric: tabular-nums; }
|
|
81
|
+
.group-button:hover { background: var(--subtle); border-color: var(--line-strong); }
|
|
82
|
+
.group-button[aria-pressed="true"] { color: #fff; background: var(--accent); border-color: var(--accent); }
|
|
83
|
+
.group-description { color: var(--muted); font-size: 14px; padding: 0 0 14px; }
|
|
84
|
+
.matrix-scroll { overflow-x: auto; border: 1px solid var(--line); border-radius: var(--radius); }
|
|
85
|
+
.matrix { width: 100%; border-collapse: collapse; text-align: left; table-layout: auto; }
|
|
86
|
+
.matrix th, .matrix td { padding: 13px 22px; border-right: 1px solid var(--line); overflow-wrap: anywhere; }
|
|
87
|
+
.matrix tr > :last-child { border-right: 0; }
|
|
88
|
+
.matrix > thead th { font-weight: 500; background: var(--subtle); height: 75px; padding-top: 5px; padding-bottom: 5px; vertical-align: middle; }
|
|
89
|
+
.matrix > thead .run-heading { min-width: 118px; text-align: center; max-width: 220px; }
|
|
90
|
+
.run-heading > span { display: block; font-size: 14px; font-weight: 400; }
|
|
91
|
+
.run-heading, .run-cell { font-variant-numeric: tabular-nums; }
|
|
92
|
+
.matrix .expand-cell { width: 64px; min-width: 56px; padding: 7px 8px; text-align: center; }
|
|
93
|
+
.name-heading, .name-cell { min-width: 220px; }
|
|
94
|
+
.namespace-heading, .namespace-cell { min-width: 190px; }
|
|
95
|
+
.state-heading, .state-cell { min-width: 134px; }
|
|
96
|
+
.attack-heading, .attack-cell { min-width: 106px; }
|
|
97
|
+
.identifier-token { display: inline-block; white-space: nowrap; }
|
|
98
|
+
.priority-heading, .priority-cell { min-width: 100px; }
|
|
99
|
+
.capability-row { height: 54px; border-top: 1px solid var(--line); cursor: pointer; }
|
|
100
|
+
.capability-row th { font-weight: 400; }
|
|
101
|
+
.capability-row:hover { background: var(--subtle); }
|
|
102
|
+
.capability-row.selected { background: var(--selected); border-top-color: var(--line-strong); }
|
|
103
|
+
.capability-row.selected > * { border-right-color: #ccd9e9; }
|
|
104
|
+
.capability-row.selected .name-cell { font-weight: 500; }
|
|
105
|
+
.row-toggle { display: inline-flex; align-items: center; justify-content: center; width: 38px; height: 38px; border: 0; border-radius: var(--radius); }
|
|
106
|
+
.row-toggle:hover { background: #dce7f6; }
|
|
107
|
+
.row-toggle .icon { width: 18px; height: 18px; transition: transform 150ms ease-out; }
|
|
108
|
+
.row-toggle[aria-expanded="true"] .icon { transform: rotate(90deg); }
|
|
109
|
+
.run-cell { text-align: center; }
|
|
110
|
+
.observation { display: inline-flex; align-items: center; }
|
|
111
|
+
.observation .icon { width: 24px; height: 24px; }
|
|
112
|
+
.observed { color: var(--observed); }
|
|
113
|
+
.missing, .neutral { color: var(--muted); }
|
|
114
|
+
.mixed { color: var(--warning); }
|
|
115
|
+
.priority-cell { text-transform: lowercase; white-space: nowrap; }
|
|
116
|
+
.priority-cell .score { margin-left: 8px; }
|
|
117
|
+
.mobile-namespace { display: none; }
|
|
118
|
+
.details-row > td { padding: 0; background: var(--subtle); border-bottom: 1px solid var(--line-strong); }
|
|
119
|
+
.finding-detail { display: grid; grid-template-columns: minmax(220px,1.3fr) minmax(205px,.85fr) minmax(240px,1.45fr); gap: 0; min-height: 212px; padding: 20px 30px 20px 84px; }
|
|
120
|
+
.finding-detail > div { min-width: 0; }
|
|
121
|
+
.evidence-column { padding-right: 46px; }
|
|
122
|
+
.rule-column { padding: 0 32px; border-left: 1px solid var(--line); }
|
|
123
|
+
.action-column { padding-left: 32px; border-left: 1px solid var(--line); }
|
|
124
|
+
.action-column > p { max-width: 70ch; line-height: 1.6; }
|
|
125
|
+
.action-column > p + p { margin-top: 12px; }
|
|
126
|
+
.evidence-source + .evidence-source { margin-top: 22px; }
|
|
127
|
+
.evidence-table { width: 100%; border-collapse: collapse; font-size: 16px; text-align: left; }
|
|
128
|
+
.evidence-table th, .evidence-table td { height: auto; padding: 3px 0; border: 0; border-bottom: 1px solid var(--line); font-weight: 400; background: transparent; }
|
|
129
|
+
.evidence-table th { color: var(--muted); }
|
|
130
|
+
.evidence-table td:last-child { width: 30px; }
|
|
131
|
+
.finding-detail code { font-size: 1em; }
|
|
132
|
+
.evidence-table code { overflow-wrap: anywhere; }
|
|
133
|
+
.icon-button { display: inline-flex; align-items: center; justify-content: center; vertical-align: middle; min-width: 28px; height: 28px; border: 0; border-radius: var(--radius); color: var(--muted); }
|
|
134
|
+
.icon-button .icon { width: 17px; height: 17px; }
|
|
135
|
+
.icon-button:hover { color: var(--accent); background: #e4ebf4; }
|
|
136
|
+
.image-base { display: flex; align-items: center; gap: 16px; margin-top: 9px; font-size: 16px; flex-wrap: wrap; }
|
|
137
|
+
.image-base .copy-button { margin-left: auto; }
|
|
138
|
+
.evidence-note { font-size: 13px; margin-top: 12px; }
|
|
139
|
+
.key-values { margin: 0; }
|
|
140
|
+
.key-values > div { display: grid; grid-template-columns: minmax(95px,.9fr) minmax(0,1.2fr); gap: 12px; padding: 3px 0; }
|
|
141
|
+
.key-values dt, .key-values dd { margin: 0; overflow-wrap: anywhere; }
|
|
142
|
+
.rule-column .key-values > div { grid-template-columns: minmax(7.25rem,max-content) minmax(0,1fr); }
|
|
143
|
+
.identifiers { border-bottom: 1px solid var(--line); padding-bottom: 6px; }
|
|
144
|
+
.scopes { padding-top: 6px; font-size: 16px; }
|
|
145
|
+
.reason-details { margin-top: 10px; font-size: 13px; }
|
|
146
|
+
.reason-details summary { color: var(--muted); width: fit-content; }
|
|
147
|
+
.reason-details p, .reason-details ul { margin-top: 10px; }
|
|
148
|
+
.reason-details ul { padding-left: 18px; }
|
|
149
|
+
.reason-details li + li { margin-top: 5px; }
|
|
150
|
+
.matrix-footer { display: flex; align-items: center; justify-content: space-between; gap: 20px; min-height: 62px; padding: 14px 0; font-size: 13px; }
|
|
151
|
+
.legend { display: flex; align-items: center; gap: 18px; margin-left: auto; }
|
|
152
|
+
.legend > span { display: flex; align-items: center; gap: 7px; white-space: nowrap; }
|
|
153
|
+
.legend > span > span { color: var(--ink); }
|
|
154
|
+
.boundary { color: var(--muted); padding-left: 18px; border-left: 1px solid var(--line); }
|
|
155
|
+
.report-sections { margin: 0 22px 26px; border: 1px solid var(--line); border-radius: var(--radius); overflow: hidden; }
|
|
156
|
+
.report-section + .report-section { border-top: 1px solid var(--line); }
|
|
157
|
+
.report-section { scroll-margin-top: 20px; }
|
|
158
|
+
.report-section > summary { display: flex; align-items: center; gap: 16px; padding: 13px 20px; list-style: none; min-height: 49px; }
|
|
159
|
+
.report-section > summary::-webkit-details-marker { display: none; }
|
|
160
|
+
.report-section > summary .icon { width: 17px; height: 17px; }
|
|
161
|
+
.report-section[open] > summary .icon { transform: rotate(90deg); }
|
|
162
|
+
.report-section > summary:hover { background: var(--subtle); }
|
|
163
|
+
.report-section[open] > summary { background: var(--subtle); font-weight: 500; border-bottom: 1px solid var(--line); }
|
|
164
|
+
.section-content { padding: 22px 26px; }
|
|
165
|
+
.section-intro { color: var(--muted); max-width: 80ch; margin-bottom: 20px; }
|
|
166
|
+
.section-content p + p { margin-top: 12px; }
|
|
167
|
+
.section-content ul { margin: 0 0 14px; padding-left: 20px; }
|
|
168
|
+
.horizontal-scroll { overflow-x: auto; }
|
|
169
|
+
.data-table { width: 100%; border-collapse: collapse; text-align: left; font-size: 14px; }
|
|
170
|
+
.data-table th, .data-table td { padding: 12px 14px; border-bottom: 1px solid var(--line); vertical-align: top; }
|
|
171
|
+
.data-table thead th { font-weight: 500; color: var(--muted); }
|
|
172
|
+
.data-table tbody th { font-weight: 500; }
|
|
173
|
+
.data-table .icon-button { margin-left: 9px; }
|
|
174
|
+
.data-table code { overflow-wrap: anywhere; }
|
|
175
|
+
.text-button { border: 0; text-align: left; color: var(--accent); padding: 4px 0; text-underline-offset: 3px; }
|
|
176
|
+
.text-button:hover { text-decoration: underline; }
|
|
177
|
+
.hotspot-links > .text-button { display: block; }
|
|
178
|
+
.warning-list { color: var(--warning); line-height: 1.65; }
|
|
179
|
+
.warning-list li + li { margin-top: 7px; }
|
|
180
|
+
.input-documents { display: grid; grid-template-columns: repeat(auto-fit,minmax(min(100%,330px),1fr)); gap: 32px; margin-bottom: 26px; }
|
|
181
|
+
.input-document { min-width: 0; }
|
|
182
|
+
.input-document > h3 { margin-bottom: 14px; }
|
|
183
|
+
.input-document .key-values { font-size: 13px; }
|
|
184
|
+
.hash-line { display: flex; align-items: start; flex-wrap: wrap; gap: 9px; font-size: 13px; margin-bottom: 12px; }
|
|
185
|
+
.hash-line > code { overflow-wrap: anywhere; flex-basis: 100%; order: 3; }
|
|
186
|
+
.empty-state { text-align: center; padding: 48px 20px; border: 1px solid var(--line); border-top: 0; }
|
|
187
|
+
.empty-state p { margin: 10px auto 20px; color: var(--muted); max-width: 60ch; }
|
|
188
|
+
.no-script { padding: 15px 0; }
|
|
189
|
+
.toast { position: fixed; bottom: 22px; right: 22px; max-width: min(360px,calc(100% - 44px)); padding: 12px 18px; border: 1px solid var(--line-strong); border-radius: var(--radius); background: var(--surface); color: var(--ink); z-index: 5; }
|
|
190
|
+
.copy-dialog { border: 1px solid var(--line); border-radius: 6px; background: var(--surface); color: var(--ink); max-width: 600px; width: calc(100% - 40px); padding: 24px; }
|
|
191
|
+
.copy-dialog::backdrop { background: rgb(20 30 45 / 28%); }
|
|
192
|
+
.dialog-heading { display: flex; align-items: center; justify-content: space-between; margin-bottom: 16px; }
|
|
193
|
+
.copy-dialog p { margin-bottom: 16px; color: var(--muted); }
|
|
194
|
+
.copy-dialog textarea { width: 100%; min-height: 120px; resize: vertical; padding: 12px; border: 1px solid var(--line); background: var(--subtle); border-radius: var(--radius); }
|
|
195
|
+
|
|
196
|
+
@media (max-width: 1250px) {
|
|
197
|
+
.sample-strip { flex-wrap: wrap; gap: 14px; }
|
|
198
|
+
.coverage-summary { font-size: 14px; }
|
|
199
|
+
.namespace-cell, .namespace-heading { display: none; }
|
|
200
|
+
.mobile-namespace { display: block; color: var(--muted); font-size: 12px; font-weight: 400; margin-top: 4px; overflow-wrap: anywhere; }
|
|
201
|
+
.matrix th, .matrix td { padding-left: 16px; padding-right: 16px; }
|
|
202
|
+
.matrix .expand-cell { width: 52px; min-width: 46px; padding-left: 6px; padding-right: 6px; }
|
|
203
|
+
.details-row > td { padding: 0; }
|
|
204
|
+
.finding-detail { padding-left: 56px; padding-right: 22px; grid-template-columns: minmax(190px,1fr) minmax(170px,.85fr) minmax(210px,1.3fr); }
|
|
205
|
+
.evidence-column { padding-right: 24px; }
|
|
206
|
+
.rule-column { padding: 0 24px; }
|
|
207
|
+
.action-column { padding-left: 24px; }
|
|
208
|
+
.search-field { width: 260px; }
|
|
209
|
+
.workspace-toolbar nav { gap: 8px; }
|
|
210
|
+
.matrix-footer { flex-wrap: wrap; gap: 10px 20px; }
|
|
211
|
+
}
|
|
212
|
+
@media (max-width: 960px) {
|
|
213
|
+
.workspace-toolbar { flex-wrap: wrap; padding-bottom: 14px; gap: 10px; }
|
|
214
|
+
.workspace-toolbar nav { width: 100%; min-height: 56px; }
|
|
215
|
+
.filters { width: 100%; }
|
|
216
|
+
.search-field { width: auto; flex: 1; }
|
|
217
|
+
.matrix th, .matrix td { padding-left: 12px; padding-right: 12px; }
|
|
218
|
+
.details-row > td { padding: 0; }
|
|
219
|
+
.matrix > thead .run-heading { min-width: 100px; }
|
|
220
|
+
.matrix .name-heading, .matrix .name-cell { min-width: 210px; }
|
|
221
|
+
.finding-detail { grid-template-columns: 1.15fr 1fr; gap: 22px 0; padding: 24px; }
|
|
222
|
+
.action-column { border: 0; border-top: 1px solid var(--line); padding: 18px 0 0; grid-column: 1 / -1; }
|
|
223
|
+
.boundary { padding-left: 0; border: 0; }
|
|
224
|
+
.legend { margin-left: 0; }
|
|
225
|
+
}
|
|
226
|
+
@media (max-width: 600px) {
|
|
227
|
+
:root { font-size: 14px; }
|
|
228
|
+
h3 { font-size: 15px; margin-bottom: 12px; }
|
|
229
|
+
.finding-detail code { font-size: .93em; }
|
|
230
|
+
.identifier-token { white-space: normal; overflow-wrap: anywhere; }
|
|
231
|
+
.image-base { font-size: 14px; }
|
|
232
|
+
.identifiers { padding-bottom: 10px; }
|
|
233
|
+
.scopes { padding-top: 9px; font-size: 14px; }
|
|
234
|
+
.reason-details { margin-top: 14px; }
|
|
235
|
+
.appbar { min-height: 64px; padding: 13px 16px; gap: 12px; }
|
|
236
|
+
.wordmark { font-size: 24px; }
|
|
237
|
+
.brand { gap: 10px; }
|
|
238
|
+
.sample-name { display: none; }
|
|
239
|
+
.button { font-size: 13px; min-height: 40px; }
|
|
240
|
+
.sample-strip { padding: 16px; }
|
|
241
|
+
.sample-metadata, .coverage-summary { gap: 8px 14px; font-size: 13px; }
|
|
242
|
+
.sample-metadata > span, .coverage-summary > span, .coverage-summary > a { padding-right: 12px; }
|
|
243
|
+
.sample-metadata small { margin-right: 4px; }
|
|
244
|
+
.workspace-toolbar { margin: 0 16px; }
|
|
245
|
+
.workspace-toolbar nav { justify-content: space-between; gap: 0; min-height: 54px; }
|
|
246
|
+
.workspace-toolbar nav a { padding: 12px 5px; font-size: 13px; }
|
|
247
|
+
.filters { gap: 8px; }
|
|
248
|
+
.search-field { min-height: 42px; padding: 0 9px; gap: 7px; }
|
|
249
|
+
.search-field input { font-size: 14px; }
|
|
250
|
+
.state-filter select { width: 133px; padding-right: 20px; font-size: 13px; min-height: 42px; }
|
|
251
|
+
.matrix-section { margin: 0 16px; }
|
|
252
|
+
.groupbar { gap: 8px; min-height: 66px; }
|
|
253
|
+
.group-button { padding: 8px 10px; gap: 10px; font-size: 13px; min-height: 42px; }
|
|
254
|
+
.matrix-scroll { border: 0; border-bottom: 1px solid var(--line); overflow: visible; }
|
|
255
|
+
.matrix { display: block; }
|
|
256
|
+
.matrix > thead { display: none; }
|
|
257
|
+
.matrix-group { display: block; border: 1px solid var(--line); border-bottom: 0; }
|
|
258
|
+
.capability-row { display: grid; grid-template-columns: 42px minmax(0,1fr) minmax(0,1fr); height: auto; padding: 8px 10px 12px 0; gap: 6px 0; border: 0; }
|
|
259
|
+
.matrix .capability-row > * { padding: 4px 8px; min-width: 0; border: 0; }
|
|
260
|
+
.matrix .capability-row .expand-cell { grid-column: 1; grid-row: 1; padding: 0; min-width: 42px; width: auto; }
|
|
261
|
+
.matrix .name-cell { grid-column: 2 / -1; font-size: 14px; min-width: 0; align-self: center; text-align: left; }
|
|
262
|
+
.matrix .namespace-cell { display: none; }
|
|
263
|
+
.state-cell { grid-column: 2 / -1; font-size: 12px; color: var(--muted); }
|
|
264
|
+
.run-cell, .attack-cell, .priority-cell { display: flex; align-items: center; justify-content: space-between; gap: 12px; font-size: 12px; text-align: left; white-space: normal; }
|
|
265
|
+
.run-cell { grid-column: 2 / -1; }
|
|
266
|
+
.run-cell::before, .attack-cell::before, .priority-cell::before { content: attr(data-label); color: var(--muted); overflow-wrap: anywhere; }
|
|
267
|
+
.observation::after { margin-left: 7px; content: ""; }
|
|
268
|
+
.run-cell.observed .observation::after { content: "Observed"; }
|
|
269
|
+
.run-cell.missing .observation::after { content: "Missing"; }
|
|
270
|
+
.run-cell.neutral .observation::after { content: "Not comparable"; }
|
|
271
|
+
.observation .icon { width: 18px; height: 18px; }
|
|
272
|
+
.attack-cell { grid-column: 2; flex-wrap: wrap; justify-content: flex-start; gap: 4px 8px; }
|
|
273
|
+
.priority-cell { grid-column: 3; justify-content: flex-end; gap: 8px; }
|
|
274
|
+
.priority-cell .score { margin: 0; }
|
|
275
|
+
.details-row, .details-row > td { display: block; width: 100%; }
|
|
276
|
+
.finding-detail { display: block; min-height: 0; padding: 20px; border-top: 1px solid var(--line-strong); }
|
|
277
|
+
.evidence-column, .rule-column, .action-column { padding: 0; border: 0; }
|
|
278
|
+
.rule-column, .action-column { margin-top: 22px; padding-top: 18px; border-top: 1px solid var(--line); }
|
|
279
|
+
.evidence-table { font-size: 13px; }
|
|
280
|
+
.evidence-table th, .evidence-table td { padding: 8px 0; }
|
|
281
|
+
.icon-button { min-width: 32px; height: 32px; }
|
|
282
|
+
.key-values > div { grid-template-columns: 1fr 1fr; padding: 7px 0; }
|
|
283
|
+
.rule-column .key-values > div { grid-template-columns: 1fr 1fr; }
|
|
284
|
+
.matrix-footer { padding: 16px 0; font-size: 12px; gap: 12px; }
|
|
285
|
+
.legend { width: 100%; justify-content: space-between; gap: 8px; }
|
|
286
|
+
.legend > span { gap: 6px; }
|
|
287
|
+
.legend .icon { width: 18px; height: 18px; }
|
|
288
|
+
.boundary { line-height: 1.5; }
|
|
289
|
+
.report-sections { margin: 0 16px 24px; }
|
|
290
|
+
.report-section > summary { min-height: 50px; padding: 13px 16px; gap: 12px; }
|
|
291
|
+
.section-content { padding: 18px 16px; }
|
|
292
|
+
.data-table { min-width: 520px; font-size: 13px; }
|
|
293
|
+
.input-document .key-values > div { display: block; }
|
|
294
|
+
.input-document dt { color: var(--muted); margin-bottom: 4px; }
|
|
295
|
+
.input-documents { gap: 24px; }
|
|
296
|
+
}
|
|
297
|
+
@media (prefers-reduced-motion: reduce) {
|
|
298
|
+
*, *::before, *::after { scroll-behavior: auto !important; transition: none !important; animation: none !important; }
|
|
299
|
+
}
|
|
300
|
+
@media print {
|
|
301
|
+
:root { font-size: 10pt; }
|
|
302
|
+
.js-only, .row-toggle, .filters, .workspace-toolbar, .groupbar, .skip-link, .copy-dialog, .toast { display: none !important; }
|
|
303
|
+
.print-only { display: inline; }
|
|
304
|
+
.matrix-group[hidden] { display: table-row-group !important; }
|
|
305
|
+
.details-row[hidden] { display: table-row !important; }
|
|
306
|
+
.matrix-scroll, .horizontal-scroll { overflow: visible; border-radius: 0; }
|
|
307
|
+
.matrix { min-width: 0; }
|
|
308
|
+
.matrix th, .matrix td { min-width: 0; padding: 7px; }
|
|
309
|
+
.namespace-cell, .namespace-heading { display: none; }
|
|
310
|
+
.mobile-namespace { display: block; }
|
|
311
|
+
.finding-detail { min-height: 0; padding: 16px; font-size: 9pt; }
|
|
312
|
+
.matrix-group, .report-section { break-inside: avoid; }
|
|
313
|
+
.empty-state { display: none !important; }
|
|
314
|
+
.appbar { padding: 0 0 14px; }
|
|
315
|
+
.sample-strip { padding: 14px 0; }
|
|
316
|
+
.matrix-section, .report-sections { margin-left: 0; margin-right: 0; }
|
|
317
|
+
}
|