kekkai-cli 1.0.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.
- kekkai/__init__.py +7 -0
- kekkai/cli.py +1038 -0
- kekkai/config.py +403 -0
- kekkai/dojo.py +419 -0
- kekkai/dojo_import.py +213 -0
- kekkai/github/__init__.py +16 -0
- kekkai/github/commenter.py +198 -0
- kekkai/github/models.py +56 -0
- kekkai/github/sanitizer.py +112 -0
- kekkai/installer/__init__.py +39 -0
- kekkai/installer/errors.py +23 -0
- kekkai/installer/extract.py +161 -0
- kekkai/installer/manager.py +252 -0
- kekkai/installer/manifest.py +189 -0
- kekkai/installer/verify.py +86 -0
- kekkai/manifest.py +77 -0
- kekkai/output.py +218 -0
- kekkai/paths.py +46 -0
- kekkai/policy.py +326 -0
- kekkai/runner.py +70 -0
- kekkai/scanners/__init__.py +67 -0
- kekkai/scanners/backends/__init__.py +14 -0
- kekkai/scanners/backends/base.py +73 -0
- kekkai/scanners/backends/docker.py +178 -0
- kekkai/scanners/backends/native.py +240 -0
- kekkai/scanners/base.py +110 -0
- kekkai/scanners/container.py +144 -0
- kekkai/scanners/falco.py +237 -0
- kekkai/scanners/gitleaks.py +237 -0
- kekkai/scanners/semgrep.py +227 -0
- kekkai/scanners/trivy.py +246 -0
- kekkai/scanners/url_policy.py +163 -0
- kekkai/scanners/zap.py +340 -0
- kekkai/threatflow/__init__.py +94 -0
- kekkai/threatflow/artifacts.py +476 -0
- kekkai/threatflow/chunking.py +361 -0
- kekkai/threatflow/core.py +438 -0
- kekkai/threatflow/mermaid.py +374 -0
- kekkai/threatflow/model_adapter.py +491 -0
- kekkai/threatflow/prompts.py +277 -0
- kekkai/threatflow/redaction.py +228 -0
- kekkai/threatflow/sanitizer.py +643 -0
- kekkai/triage/__init__.py +33 -0
- kekkai/triage/app.py +168 -0
- kekkai/triage/audit.py +203 -0
- kekkai/triage/ignore.py +269 -0
- kekkai/triage/models.py +185 -0
- kekkai/triage/screens.py +341 -0
- kekkai/triage/widgets.py +169 -0
- kekkai_cli-1.0.0.dist-info/METADATA +135 -0
- kekkai_cli-1.0.0.dist-info/RECORD +90 -0
- kekkai_cli-1.0.0.dist-info/WHEEL +5 -0
- kekkai_cli-1.0.0.dist-info/entry_points.txt +3 -0
- kekkai_cli-1.0.0.dist-info/top_level.txt +3 -0
- kekkai_core/__init__.py +3 -0
- kekkai_core/ci/__init__.py +11 -0
- kekkai_core/ci/benchmarks.py +354 -0
- kekkai_core/ci/metadata.py +104 -0
- kekkai_core/ci/validators.py +92 -0
- kekkai_core/docker/__init__.py +17 -0
- kekkai_core/docker/metadata.py +153 -0
- kekkai_core/docker/sbom.py +173 -0
- kekkai_core/docker/security.py +158 -0
- kekkai_core/docker/signing.py +135 -0
- kekkai_core/redaction.py +84 -0
- kekkai_core/slsa/__init__.py +13 -0
- kekkai_core/slsa/verify.py +121 -0
- kekkai_core/windows/__init__.py +29 -0
- kekkai_core/windows/chocolatey.py +335 -0
- kekkai_core/windows/installer.py +256 -0
- kekkai_core/windows/scoop.py +165 -0
- kekkai_core/windows/validators.py +220 -0
- portal/__init__.py +19 -0
- portal/api.py +155 -0
- portal/auth.py +103 -0
- portal/enterprise/__init__.py +32 -0
- portal/enterprise/audit.py +435 -0
- portal/enterprise/licensing.py +342 -0
- portal/enterprise/rbac.py +276 -0
- portal/enterprise/saml.py +595 -0
- portal/ops/__init__.py +53 -0
- portal/ops/backup.py +553 -0
- portal/ops/log_shipper.py +469 -0
- portal/ops/monitoring.py +517 -0
- portal/ops/restore.py +469 -0
- portal/ops/secrets.py +408 -0
- portal/ops/upgrade.py +591 -0
- portal/tenants.py +340 -0
- portal/uploads.py +259 -0
- portal/web.py +384 -0
|
@@ -0,0 +1,374 @@
|
|
|
1
|
+
"""Mermaid.js DFD generation for ThreatFlow.
|
|
2
|
+
|
|
3
|
+
Generates Mermaid.js syntax for Data Flow Diagrams (DFDs) with security encoding
|
|
4
|
+
to prevent XSS and injection attacks in rendered diagrams.
|
|
5
|
+
|
|
6
|
+
ASVS 5.0 Requirements:
|
|
7
|
+
- V5.3.3: Output encoding for target format
|
|
8
|
+
- V5.2.6: Validate structured output
|
|
9
|
+
- V5.5.2: Safe serialization (no executable content)
|
|
10
|
+
|
|
11
|
+
Threat Mitigations:
|
|
12
|
+
- XSS payloads in labels -> HTML escape + unsafe char replacement
|
|
13
|
+
- Mermaid syntax injection -> Strip special characters
|
|
14
|
+
- Architecture spoofing -> Validate against known entities
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import html
|
|
20
|
+
import re
|
|
21
|
+
from dataclasses import dataclass, field
|
|
22
|
+
from enum import Enum
|
|
23
|
+
from typing import TYPE_CHECKING
|
|
24
|
+
|
|
25
|
+
if TYPE_CHECKING:
|
|
26
|
+
from .artifacts import ThreatModelArtifacts
|
|
27
|
+
|
|
28
|
+
# Characters unsafe in Mermaid labels that could break syntax or enable injection
|
|
29
|
+
MERMAID_UNSAFE_CHARS = re.compile(r'[<>"\'`{}|\\;\[\]()]')
|
|
30
|
+
|
|
31
|
+
# Maximum label length to prevent DoS via extremely long labels
|
|
32
|
+
MAX_LABEL_LENGTH = 100
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class NodeType(Enum):
|
|
36
|
+
"""DFD node types with corresponding Mermaid shapes."""
|
|
37
|
+
|
|
38
|
+
EXTERNAL_ENTITY = "external_entity"
|
|
39
|
+
PROCESS = "process"
|
|
40
|
+
DATA_STORE = "data_store"
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@dataclass(frozen=True)
|
|
44
|
+
class MermaidNode:
|
|
45
|
+
"""A node in the Mermaid DFD.
|
|
46
|
+
|
|
47
|
+
Attributes:
|
|
48
|
+
id: Unique identifier for the node (alphanumeric + underscore only)
|
|
49
|
+
label: Display label (will be sanitized)
|
|
50
|
+
node_type: Type of DFD element
|
|
51
|
+
"""
|
|
52
|
+
|
|
53
|
+
id: str
|
|
54
|
+
label: str
|
|
55
|
+
node_type: NodeType
|
|
56
|
+
|
|
57
|
+
def to_mermaid(self) -> str:
|
|
58
|
+
"""Convert to Mermaid syntax with security encoding.
|
|
59
|
+
|
|
60
|
+
Returns:
|
|
61
|
+
Mermaid node definition string
|
|
62
|
+
"""
|
|
63
|
+
safe_label = _encode_label(self.label)
|
|
64
|
+
safe_id = _sanitize_id(self.id)
|
|
65
|
+
|
|
66
|
+
# Map node types to Mermaid shapes
|
|
67
|
+
# External entities: parallelogram (trapezoid)
|
|
68
|
+
# Processes: circle (stadium shape)
|
|
69
|
+
# Data stores: cylinder
|
|
70
|
+
shapes = {
|
|
71
|
+
NodeType.EXTERNAL_ENTITY: f'{safe_id}[/"{safe_label}"/]',
|
|
72
|
+
NodeType.PROCESS: f'{safe_id}(["{safe_label}"])',
|
|
73
|
+
NodeType.DATA_STORE: f'{safe_id}[("{safe_label}")]',
|
|
74
|
+
}
|
|
75
|
+
return shapes.get(self.node_type, f'{safe_id}["{safe_label}"]')
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
@dataclass(frozen=True)
|
|
79
|
+
class MermaidEdge:
|
|
80
|
+
"""An edge (data flow) in the Mermaid DFD.
|
|
81
|
+
|
|
82
|
+
Attributes:
|
|
83
|
+
source: Source node ID
|
|
84
|
+
target: Target node ID
|
|
85
|
+
label: Edge label describing the data flow
|
|
86
|
+
crosses_trust_boundary: Whether this flow crosses a trust boundary
|
|
87
|
+
"""
|
|
88
|
+
|
|
89
|
+
source: str
|
|
90
|
+
target: str
|
|
91
|
+
label: str
|
|
92
|
+
crosses_trust_boundary: bool = False
|
|
93
|
+
|
|
94
|
+
def to_mermaid(self) -> str:
|
|
95
|
+
"""Convert to Mermaid edge syntax with security encoding.
|
|
96
|
+
|
|
97
|
+
Returns:
|
|
98
|
+
Mermaid edge definition string
|
|
99
|
+
"""
|
|
100
|
+
safe_source = _sanitize_id(self.source)
|
|
101
|
+
safe_target = _sanitize_id(self.target)
|
|
102
|
+
safe_label = _encode_label(self.label)
|
|
103
|
+
|
|
104
|
+
# Use thick arrow for trust boundary crossings
|
|
105
|
+
if self.crosses_trust_boundary:
|
|
106
|
+
return f'{safe_source} ==>|"{safe_label}"| {safe_target}'
|
|
107
|
+
return f'{safe_source} -->|"{safe_label}"| {safe_target}'
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
@dataclass
|
|
111
|
+
class MermaidDFDGenerator:
|
|
112
|
+
"""Generates Mermaid.js Data Flow Diagrams.
|
|
113
|
+
|
|
114
|
+
Security-first design:
|
|
115
|
+
- All labels are HTML-encoded
|
|
116
|
+
- Special characters are replaced
|
|
117
|
+
- IDs are sanitized to alphanumeric only
|
|
118
|
+
"""
|
|
119
|
+
|
|
120
|
+
title: str = "Data Flow Diagram"
|
|
121
|
+
direction: str = "TB" # TB (top-bottom), LR (left-right)
|
|
122
|
+
_nodes: list[MermaidNode] = field(default_factory=list)
|
|
123
|
+
_edges: list[MermaidEdge] = field(default_factory=list)
|
|
124
|
+
_trust_boundaries: list[tuple[str, list[str]]] = field(default_factory=list)
|
|
125
|
+
|
|
126
|
+
def add_node(self, node: MermaidNode) -> None:
|
|
127
|
+
"""Add a node to the diagram."""
|
|
128
|
+
self._nodes.append(node)
|
|
129
|
+
|
|
130
|
+
def add_edge(self, edge: MermaidEdge) -> None:
|
|
131
|
+
"""Add an edge to the diagram."""
|
|
132
|
+
self._edges.append(edge)
|
|
133
|
+
|
|
134
|
+
def add_trust_boundary(self, name: str, node_ids: list[str]) -> None:
|
|
135
|
+
"""Add a trust boundary containing specified nodes."""
|
|
136
|
+
self._trust_boundaries.append((name, node_ids))
|
|
137
|
+
|
|
138
|
+
def generate(self) -> str:
|
|
139
|
+
"""Generate complete Mermaid DFD syntax.
|
|
140
|
+
|
|
141
|
+
Returns:
|
|
142
|
+
Valid Mermaid flowchart syntax
|
|
143
|
+
"""
|
|
144
|
+
lines: list[str] = []
|
|
145
|
+
|
|
146
|
+
# Header with title
|
|
147
|
+
safe_title = _encode_label(self.title)
|
|
148
|
+
lines.extend(
|
|
149
|
+
[
|
|
150
|
+
"---",
|
|
151
|
+
f"title: {safe_title}",
|
|
152
|
+
"---",
|
|
153
|
+
f"flowchart {self.direction}",
|
|
154
|
+
"",
|
|
155
|
+
]
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
# Group nodes by type for organization
|
|
159
|
+
external = [n for n in self._nodes if n.node_type == NodeType.EXTERNAL_ENTITY]
|
|
160
|
+
processes = [n for n in self._nodes if n.node_type == NodeType.PROCESS]
|
|
161
|
+
stores = [n for n in self._nodes if n.node_type == NodeType.DATA_STORE]
|
|
162
|
+
|
|
163
|
+
# Add trust boundaries as subgraphs
|
|
164
|
+
nodes_in_boundaries: set[str] = set()
|
|
165
|
+
for boundary_name, node_ids in self._trust_boundaries:
|
|
166
|
+
safe_boundary = _sanitize_id(boundary_name)
|
|
167
|
+
safe_label = _encode_label(boundary_name)
|
|
168
|
+
lines.append(f' subgraph {safe_boundary}["{safe_label}"]')
|
|
169
|
+
for node_id in node_ids:
|
|
170
|
+
nodes_in_boundaries.add(node_id)
|
|
171
|
+
node = self._find_node(node_id)
|
|
172
|
+
if node:
|
|
173
|
+
lines.append(f" {node.to_mermaid()}")
|
|
174
|
+
lines.append(" end")
|
|
175
|
+
lines.append("")
|
|
176
|
+
|
|
177
|
+
# Add remaining nodes not in boundaries
|
|
178
|
+
if external:
|
|
179
|
+
lines.append(" %% External Entities")
|
|
180
|
+
for node in external:
|
|
181
|
+
if node.id not in nodes_in_boundaries:
|
|
182
|
+
lines.append(f" {node.to_mermaid()}")
|
|
183
|
+
lines.append("")
|
|
184
|
+
|
|
185
|
+
if processes:
|
|
186
|
+
lines.append(" %% Processes")
|
|
187
|
+
for node in processes:
|
|
188
|
+
if node.id not in nodes_in_boundaries:
|
|
189
|
+
lines.append(f" {node.to_mermaid()}")
|
|
190
|
+
lines.append("")
|
|
191
|
+
|
|
192
|
+
if stores:
|
|
193
|
+
lines.append(" %% Data Stores")
|
|
194
|
+
for node in stores:
|
|
195
|
+
if node.id not in nodes_in_boundaries:
|
|
196
|
+
lines.append(f" {node.to_mermaid()}")
|
|
197
|
+
lines.append("")
|
|
198
|
+
|
|
199
|
+
# Add edges
|
|
200
|
+
if self._edges:
|
|
201
|
+
lines.append(" %% Data Flows")
|
|
202
|
+
for edge in self._edges:
|
|
203
|
+
lines.append(f" {edge.to_mermaid()}")
|
|
204
|
+
lines.append("")
|
|
205
|
+
|
|
206
|
+
# Add styling for trust boundary crossings
|
|
207
|
+
boundary_edges = [e for e in self._edges if e.crosses_trust_boundary]
|
|
208
|
+
if boundary_edges:
|
|
209
|
+
lines.append(" %% Style trust boundary crossings")
|
|
210
|
+
lines.append(" linkStyle default stroke:#333,stroke-width:2px")
|
|
211
|
+
|
|
212
|
+
return "\n".join(lines)
|
|
213
|
+
|
|
214
|
+
def _find_node(self, node_id: str) -> MermaidNode | None:
|
|
215
|
+
"""Find a node by ID."""
|
|
216
|
+
for node in self._nodes:
|
|
217
|
+
if node.id == node_id:
|
|
218
|
+
return node
|
|
219
|
+
return None
|
|
220
|
+
|
|
221
|
+
@classmethod
|
|
222
|
+
def from_artifacts(cls, artifacts: ThreatModelArtifacts) -> MermaidDFDGenerator:
|
|
223
|
+
"""Create a Mermaid DFD generator from ThreatModelArtifacts.
|
|
224
|
+
|
|
225
|
+
Args:
|
|
226
|
+
artifacts: ThreatModelArtifacts containing DFD components
|
|
227
|
+
|
|
228
|
+
Returns:
|
|
229
|
+
Configured MermaidDFDGenerator
|
|
230
|
+
"""
|
|
231
|
+
generator = cls(title=f"{artifacts.repo_name} Data Flow Diagram")
|
|
232
|
+
|
|
233
|
+
# Track node IDs for edge validation
|
|
234
|
+
node_ids: set[str] = set()
|
|
235
|
+
|
|
236
|
+
# Add external entities
|
|
237
|
+
for i, entity in enumerate(artifacts.external_entities):
|
|
238
|
+
node_id = f"ext_{i}"
|
|
239
|
+
generator.add_node(
|
|
240
|
+
MermaidNode(
|
|
241
|
+
id=node_id,
|
|
242
|
+
label=entity,
|
|
243
|
+
node_type=NodeType.EXTERNAL_ENTITY,
|
|
244
|
+
)
|
|
245
|
+
)
|
|
246
|
+
node_ids.add(node_id)
|
|
247
|
+
|
|
248
|
+
# Add processes
|
|
249
|
+
for i, process in enumerate(artifacts.processes):
|
|
250
|
+
node_id = f"proc_{i}"
|
|
251
|
+
generator.add_node(
|
|
252
|
+
MermaidNode(
|
|
253
|
+
id=node_id,
|
|
254
|
+
label=process,
|
|
255
|
+
node_type=NodeType.PROCESS,
|
|
256
|
+
)
|
|
257
|
+
)
|
|
258
|
+
node_ids.add(node_id)
|
|
259
|
+
|
|
260
|
+
# Add data stores
|
|
261
|
+
for i, store in enumerate(artifacts.data_stores):
|
|
262
|
+
node_id = f"store_{i}"
|
|
263
|
+
generator.add_node(
|
|
264
|
+
MermaidNode(
|
|
265
|
+
id=node_id,
|
|
266
|
+
label=store,
|
|
267
|
+
node_type=NodeType.DATA_STORE,
|
|
268
|
+
)
|
|
269
|
+
)
|
|
270
|
+
node_ids.add(node_id)
|
|
271
|
+
|
|
272
|
+
# Build lookup for node names to IDs
|
|
273
|
+
name_to_id = _build_name_to_id_map(generator._nodes)
|
|
274
|
+
|
|
275
|
+
# Add data flows as edges
|
|
276
|
+
for flow in artifacts.dataflows:
|
|
277
|
+
source_id = name_to_id.get(flow.source.lower(), _sanitize_id(flow.source))
|
|
278
|
+
target_id = name_to_id.get(flow.destination.lower(), _sanitize_id(flow.destination))
|
|
279
|
+
generator.add_edge(
|
|
280
|
+
MermaidEdge(
|
|
281
|
+
source=source_id,
|
|
282
|
+
target=target_id,
|
|
283
|
+
label=flow.data_type,
|
|
284
|
+
crosses_trust_boundary=flow.trust_boundary_crossed,
|
|
285
|
+
)
|
|
286
|
+
)
|
|
287
|
+
|
|
288
|
+
# Add trust boundaries
|
|
289
|
+
if artifacts.trust_boundaries:
|
|
290
|
+
# Group internal processes
|
|
291
|
+
internal_ids = [f"proc_{i}" for i in range(len(artifacts.processes))]
|
|
292
|
+
if internal_ids:
|
|
293
|
+
generator.add_trust_boundary("Internal_Network", internal_ids)
|
|
294
|
+
|
|
295
|
+
return generator
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
def _encode_label(label: str) -> str:
|
|
299
|
+
"""Encode a label for safe use in Mermaid diagrams.
|
|
300
|
+
|
|
301
|
+
Applies:
|
|
302
|
+
1. HTML escaping for XSS prevention
|
|
303
|
+
2. Replacement of Mermaid-unsafe characters
|
|
304
|
+
3. Length truncation
|
|
305
|
+
|
|
306
|
+
Args:
|
|
307
|
+
label: Raw label text
|
|
308
|
+
|
|
309
|
+
Returns:
|
|
310
|
+
Sanitized label safe for Mermaid
|
|
311
|
+
"""
|
|
312
|
+
# HTML escape first
|
|
313
|
+
safe = html.escape(label, quote=True)
|
|
314
|
+
|
|
315
|
+
# Replace unsafe Mermaid characters with underscores
|
|
316
|
+
safe = MERMAID_UNSAFE_CHARS.sub("_", safe)
|
|
317
|
+
|
|
318
|
+
# Truncate to prevent DoS
|
|
319
|
+
if len(safe) > MAX_LABEL_LENGTH:
|
|
320
|
+
safe = safe[: MAX_LABEL_LENGTH - 3] + "..."
|
|
321
|
+
|
|
322
|
+
return safe
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
def _sanitize_id(id_str: str) -> str:
|
|
326
|
+
"""Sanitize a node/subgraph ID for Mermaid.
|
|
327
|
+
|
|
328
|
+
IDs must be alphanumeric with underscores only.
|
|
329
|
+
|
|
330
|
+
Args:
|
|
331
|
+
id_str: Raw ID string
|
|
332
|
+
|
|
333
|
+
Returns:
|
|
334
|
+
Sanitized ID safe for Mermaid
|
|
335
|
+
"""
|
|
336
|
+
# Replace spaces and special chars with underscores
|
|
337
|
+
safe = re.sub(r"[^a-zA-Z0-9_]", "_", id_str)
|
|
338
|
+
|
|
339
|
+
# Ensure starts with letter (Mermaid requirement)
|
|
340
|
+
if safe and not safe[0].isalpha():
|
|
341
|
+
safe = "n_" + safe
|
|
342
|
+
|
|
343
|
+
# Ensure not empty
|
|
344
|
+
if not safe:
|
|
345
|
+
safe = "node"
|
|
346
|
+
|
|
347
|
+
return safe
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
def _build_name_to_id_map(nodes: list[MermaidNode]) -> dict[str, str]:
|
|
351
|
+
"""Build a mapping from node labels (lowercase) to IDs.
|
|
352
|
+
|
|
353
|
+
Args:
|
|
354
|
+
nodes: List of MermaidNodes
|
|
355
|
+
|
|
356
|
+
Returns:
|
|
357
|
+
Dictionary mapping lowercase labels to node IDs
|
|
358
|
+
"""
|
|
359
|
+
return {node.label.lower(): node.id for node in nodes}
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
def generate_dfd_mermaid(artifacts: ThreatModelArtifacts) -> str:
|
|
363
|
+
"""Generate Mermaid DFD syntax from ThreatModelArtifacts.
|
|
364
|
+
|
|
365
|
+
Convenience function for simple usage.
|
|
366
|
+
|
|
367
|
+
Args:
|
|
368
|
+
artifacts: ThreatModelArtifacts containing DFD components
|
|
369
|
+
|
|
370
|
+
Returns:
|
|
371
|
+
Mermaid flowchart syntax string
|
|
372
|
+
"""
|
|
373
|
+
generator = MermaidDFDGenerator.from_artifacts(artifacts)
|
|
374
|
+
return generator.generate()
|