Signet Forge 0.1.0
C++20 Parquet library with AI-native extensions
DEMO
Loading...
Searching...
No Matches
threat_model.hpp
Go to the documentation of this file.
1// SPDX-License-Identifier: AGPL-3.0-or-later
2// Copyright 2026 Johnson Ogundeji
3// See LICENSE_COMMERCIAL for full terms.
4#pragma once
5
6#if !defined(SIGNET_ENABLE_COMMERCIAL) || !SIGNET_ENABLE_COMMERCIAL
7#error "signet/ai/threat_model.hpp requires SIGNET_ENABLE_COMMERCIAL=ON (AGPL-3.0 commercial tier). See LICENSE_COMMERCIAL."
8#endif
9
10// ---------------------------------------------------------------------------
11// threat_model.hpp -- STRIDE/DREAD Threat Modeling Framework
12//
13// Gap D-12: Structured threat modeling documentation for enterprise audits.
14//
15// Provides a machine-readable threat model conforming to:
16// - Microsoft STRIDE (Spoofing, Tampering, Repudiation, Info Disclosure,
17// Denial of Service, Elevation of Privilege)
18// - DREAD risk scoring (Damage, Reproducibility, Exploitability, Affected
19// users, Discoverability) — scale 1..10, composite = mean
20// - NIST SP 800-30 Rev. 1 risk assessment methodology
21// - OWASP Threat Modeling guidelines
22//
23// Components:
24// - StrideCategory / ThreatSeverity enums
25// - DreadScore: 5-factor risk quantification
26// - ThreatEntry: individual threat with mitigations
27// - ThreatModel: collection of threats for a component
28// - ThreatModelAnalyzer: validates coverage and generates JSON report
29//
30// Header-only. Part of the signet::forge AI module.
31// ---------------------------------------------------------------------------
32
33#include "signet/error.hpp"
34
35#include <algorithm>
36#include <cstdint>
37#include <cstdio>
38#include <numeric>
39#include <sstream>
40#include <string>
41#include <vector>
42
43namespace signet::forge {
44
45// ---------------------------------------------------------------------------
46// Enumerations
47// ---------------------------------------------------------------------------
48
50enum class StrideCategory : int32_t {
51 SPOOFING = 0,
52 TAMPERING = 1,
53 REPUDIATION = 2,
57};
58
60enum class ThreatSeverity : int32_t {
61 LOW = 0,
62 MEDIUM = 1,
63 HIGH = 2,
64 CRITICAL = 3
65};
66
68enum class MitigationStatus : int32_t {
69 NOT_MITIGATED = 0,
70 PARTIAL = 1,
71 MITIGATED = 2,
72 ACCEPTED = 3,
73 TRANSFERRED = 4
74};
75
76// ---------------------------------------------------------------------------
77// DREAD Risk Score
78// ---------------------------------------------------------------------------
79
82struct DreadScore {
83 int32_t damage = 1;
84 int32_t reproducibility = 1;
85 int32_t exploitability = 1;
86 int32_t affected_users = 1;
87 int32_t discoverability = 1;
88
90 [[nodiscard]] double composite() const {
93 }
94
96 [[nodiscard]] ThreatSeverity severity() const {
97 double c = composite();
98 if (c >= 9.0) return ThreatSeverity::CRITICAL;
99 if (c >= 7.0) return ThreatSeverity::HIGH;
100 if (c >= 4.0) return ThreatSeverity::MEDIUM;
101 return ThreatSeverity::LOW;
102 }
103
105 [[nodiscard]] bool valid() const {
106 auto ok = [](int32_t v) { return v >= 1 && v <= 10; };
107 return ok(damage) && ok(reproducibility) && ok(exploitability) &&
109 }
110};
111
112// ---------------------------------------------------------------------------
113// Mitigation
114// ---------------------------------------------------------------------------
115
123
124// ---------------------------------------------------------------------------
125// ThreatEntry
126// ---------------------------------------------------------------------------
127
130 std::string threat_id;
131 std::string title;
132 std::string description;
135 std::string attack_vector;
136 std::string affected_component;
137 std::vector<Mitigation> mitigations;
138 std::vector<std::string> references;
139
141 [[nodiscard]] MitigationStatus overall_status() const {
143 auto worst = MitigationStatus::TRANSFERRED; // highest enum value
144 for (const auto& m : mitigations) {
145 if (static_cast<int32_t>(m.status) < static_cast<int32_t>(worst))
146 worst = m.status;
147 }
148 return worst;
149 }
150};
151
152// ---------------------------------------------------------------------------
153// ThreatModel
154// ---------------------------------------------------------------------------
155
158 std::string model_id;
159 std::string component;
160 std::string version;
161 std::string author;
162 std::string created_at;
163 std::string reviewed_at;
164 std::vector<ThreatEntry> threats;
165};
166
167// ---------------------------------------------------------------------------
168// ThreatModelAnalyzer
169// ---------------------------------------------------------------------------
170
173 int32_t total_threats = 0;
174 int32_t critical_count = 0;
175 int32_t high_count = 0;
176 int32_t medium_count = 0;
177 int32_t low_count = 0;
178 int32_t mitigated_count = 0;
179 int32_t unmitigated_count = 0;
180 bool stride_complete = false;
181 std::vector<StrideCategory> missing_categories;
182 double mean_dread_score = 0.0;
183 std::string report_json;
184};
185
188public:
190 [[nodiscard]] static ThreatModelAnalysis analyze(const ThreatModel& model) {
191 (void)commercial::require_feature("ThreatModelAnalyzer");
192 ThreatModelAnalysis result;
193 result.total_threats = static_cast<int32_t>(model.threats.size());
194
195 // Track STRIDE coverage
196 bool covered[6] = {};
197 double dread_sum = 0.0;
198 int32_t valid_count = 0;
199
200 for (const auto& t : model.threats) {
201 // Validate DREAD
202 if (!t.dread.valid()) continue;
203
204 ++valid_count;
205 int cat = static_cast<int32_t>(t.category);
206 if (cat >= 0 && cat < 6) covered[cat] = true;
207
208 auto sev = t.dread.severity();
209 switch (sev) {
210 case ThreatSeverity::CRITICAL: ++result.critical_count; break;
211 case ThreatSeverity::HIGH: ++result.high_count; break;
212 case ThreatSeverity::MEDIUM: ++result.medium_count; break;
213 case ThreatSeverity::LOW: ++result.low_count; break;
214 }
215
216 if (t.overall_status() >= MitigationStatus::MITIGATED)
217 ++result.mitigated_count;
218 else
219 ++result.unmitigated_count;
220
221 dread_sum += t.dread.composite();
222 }
223
224 if (valid_count > 0)
225 result.mean_dread_score = dread_sum / valid_count;
226
227 // Check STRIDE completeness
228 result.stride_complete = true;
229 for (int i = 0; i < 6; ++i) {
230 if (!covered[i]) {
231 result.stride_complete = false;
232 result.missing_categories.push_back(
233 static_cast<StrideCategory>(i));
234 }
235 }
236
237 // Generate JSON report
238 result.report_json = generate_json(model, result);
239 return result;
240 }
241
243 [[nodiscard]] static ThreatModel signet_default_model() {
244 ThreatModel m;
245 m.model_id = "SIGNET-TM-001";
246 m.component = "signet::forge";
247 m.version = "1.0.0";
248 m.author = "Signet Security Team";
249
250 // --- SPOOFING ---
251 m.threats.push_back(ThreatEntry{
252 "T-AUTH-001", "Key impersonation via INTERNAL mode",
253 "An attacker with access to plaintext keys in INTERNAL mode can "
254 "impersonate any column encryption identity.",
256 DreadScore{7, 8, 5, 6, 4},
257 "Access to unencrypted key material in file metadata",
258 "crypto/pme.hpp",
259 {Mitigation{"CTRL-KMS-001",
260 "EXTERNAL key mode with KMS integration",
261 "crypto/key_metadata.hpp:IKmsClient",
263 Mitigation{"CTRL-GATE-001",
264 "Production gate rejects INTERNAL mode (C-15)",
265 "crypto/pme.hpp:production_key_mode_gate()",
267 {"NIST SP 800-57", "PARQUET-1178"}
268 });
269
270 // --- TAMPERING ---
271 m.threats.push_back(ThreatEntry{
272 "T-TAMP-001", "Hash chain manipulation in audit logs",
273 "An attacker modifies audit chain entries without detection.",
275 DreadScore{9, 3, 4, 8, 3},
276 "Direct modification of Parquet audit log files",
277 "ai/audit_chain.hpp",
278 {Mitigation{"CTRL-CHAIN-001",
279 "SHA-256 cryptographic hash chain with prev_hash linkage",
280 "ai/audit_chain.hpp:AuditChainHasher",
282 {"SEC 17a-4", "NIST SP 800-92"}
283 });
284
285 // --- REPUDIATION ---
286 m.threats.push_back(ThreatEntry{
287 "T-REP-001", "Denial of AI decision actions",
288 "An operator denies having made or approved an AI trading decision.",
290 DreadScore{6, 7, 3, 5, 4},
291 "Lack of non-repudiable logging for human overrides",
292 "ai/decision_log.hpp",
293 {Mitigation{"CTRL-LOG-001",
294 "Immutable decision log with operator_id and hash chain",
295 "ai/decision_log.hpp:DecisionLogWriter",
297 Mitigation{"CTRL-OVER-001",
298 "Human override log with provenance (EU AI Act Art.14)",
299 "ai/human_oversight.hpp:HumanOverrideLogWriter",
301 {"EU AI Act Art.14", "MiFID II RTS 24"}
302 });
303
304 // --- INFORMATION DISCLOSURE ---
305 m.threats.push_back(ThreatEntry{
306 "T-DISC-001", "Side-channel leakage from AES timing",
307 "An attacker observes timing variations in AES operations to "
308 "recover key material.",
310 DreadScore{10, 4, 7, 3, 5},
311 "Timing analysis of AES encrypt/decrypt operations",
312 "crypto/aes_core.hpp",
313 {Mitigation{"CTRL-CT-001",
314 "Constant-time AES via bitsliced S-box + AES-NI detection",
315 "crypto/aes_core.hpp:Aes256",
317 Mitigation{"CTRL-ZERO-001",
318 "Key material zeroing in destructors",
319 "crypto/aes_core.hpp:~Aes256()",
321 {"NIST SP 800-38D", "CWE-208"}
322 });
323
324 // --- DENIAL OF SERVICE ---
325 m.threats.push_back(ThreatEntry{
326 "T-DOS-001", "Decompression bomb via crafted Parquet pages",
327 "A malicious Parquet file with extreme compression ratios causes "
328 "memory exhaustion during decompression.",
330 DreadScore{7, 9, 8, 5, 7},
331 "Crafted Parquet file with oversized uncompressed pages",
332 "reader.hpp",
333 {Mitigation{"CTRL-PAGE-001",
334 "PARQUET_MAX_PAGE_SIZE (256 MB) limit on decompressed pages",
335 "reader.hpp:PARQUET_MAX_PAGE_SIZE",
337 Mitigation{"CTRL-THRIFT-001",
338 "Thrift field count (65536) and string size (64 MB) limits",
339 "thrift/compact.hpp:MAX_FIELD_COUNT",
341 {"CWE-409", "OWASP Decompression Bomb"}
342 });
343
344 // --- ELEVATION OF PRIVILEGE ---
345 m.threats.push_back(ThreatEntry{
346 "T-PRIV-001", "Path traversal in FeatureWriter output_dir",
347 "An attacker supplies a path with '..' segments to write outside "
348 "the intended directory.",
350 DreadScore{8, 9, 7, 4, 8},
351 "Controlled output_dir parameter with path traversal sequences",
352 "ai/feature_writer.hpp",
353 {Mitigation{"CTRL-PATH-001",
354 "Path traversal guard rejects '..' segments",
355 "ai/feature_writer.hpp:create()",
357 {"CWE-22", "OWASP Path Traversal"}
358 });
359
360 return m;
361 }
362
363private:
364 static std::string stride_name(StrideCategory c) {
365 switch (c) {
366 case StrideCategory::SPOOFING: return "Spoofing";
367 case StrideCategory::TAMPERING: return "Tampering";
368 case StrideCategory::REPUDIATION: return "Repudiation";
369 case StrideCategory::INFORMATION_DISCLOSURE: return "Information Disclosure";
370 case StrideCategory::DENIAL_OF_SERVICE: return "Denial of Service";
371 case StrideCategory::ELEVATION_OF_PRIVILEGE: return "Elevation of Privilege";
372 }
373 return "Unknown";
374 }
375
376 static std::string severity_name(ThreatSeverity s) {
377 switch (s) {
378 case ThreatSeverity::LOW: return "Low";
379 case ThreatSeverity::MEDIUM: return "Medium";
380 case ThreatSeverity::HIGH: return "High";
381 case ThreatSeverity::CRITICAL: return "Critical";
382 }
383 return "Unknown";
384 }
385
386 static std::string mitigation_status_name(MitigationStatus s) {
387 switch (s) {
388 case MitigationStatus::NOT_MITIGATED: return "Not Mitigated";
389 case MitigationStatus::PARTIAL: return "Partial";
390 case MitigationStatus::MITIGATED: return "Mitigated";
391 case MitigationStatus::ACCEPTED: return "Accepted";
392 case MitigationStatus::TRANSFERRED: return "Transferred";
393 }
394 return "Unknown";
395 }
396
397 static std::string escape_json(const std::string& s) {
398 std::string out;
399 out.reserve(s.size() + 16);
400 for (char c : s) {
401 switch (c) {
402 case '"': out += "\\\""; break;
403 case '\\': out += "\\\\"; break;
404 case '\n': out += "\\n"; break;
405 case '\r': out += "\\r"; break;
406 case '\t': out += "\\t"; break;
407 default:
408 if (static_cast<unsigned char>(c) < 0x20) {
409 char buf[8];
410 std::snprintf(buf, sizeof(buf), "\\u%04x",
411 static_cast<unsigned char>(c));
412 out += buf;
413 } else {
414 out += c;
415 }
416 break;
417 }
418 }
419 return out;
420 }
421
422 static std::string generate_json(const ThreatModel& model,
423 const ThreatModelAnalysis& analysis) {
424 std::ostringstream o;
425 o << "{\n";
426 o << " \"model_id\": \"" << escape_json(model.model_id) << "\",\n";
427 o << " \"component\": \"" << escape_json(model.component) << "\",\n";
428 o << " \"version\": \"" << escape_json(model.version) << "\",\n";
429 o << " \"methodology\": \"STRIDE/DREAD\",\n";
430 o << " \"summary\": {\n";
431 o << " \"total_threats\": " << analysis.total_threats << ",\n";
432 o << " \"critical\": " << analysis.critical_count << ",\n";
433 o << " \"high\": " << analysis.high_count << ",\n";
434 o << " \"medium\": " << analysis.medium_count << ",\n";
435 o << " \"low\": " << analysis.low_count << ",\n";
436 o << " \"mitigated\": " << analysis.mitigated_count << ",\n";
437 o << " \"unmitigated\": " << analysis.unmitigated_count << ",\n";
438 o << " \"stride_complete\": " << (analysis.stride_complete ? "true" : "false") << ",\n";
439 o << " \"mean_dread_score\": " << analysis.mean_dread_score << "\n";
440 o << " },\n";
441 o << " \"threats\": [\n";
442
443 for (size_t i = 0; i < model.threats.size(); ++i) {
444 const auto& t = model.threats[i];
445 o << " {\n";
446 o << " \"id\": \"" << escape_json(t.threat_id) << "\",\n";
447 o << " \"title\": \"" << escape_json(t.title) << "\",\n";
448 o << " \"stride\": \"" << stride_name(t.category) << "\",\n";
449 o << " \"severity\": \"" << severity_name(t.dread.severity()) << "\",\n";
450 o << " \"dread\": {\n";
451 o << " \"damage\": " << t.dread.damage << ",\n";
452 o << " \"reproducibility\": " << t.dread.reproducibility << ",\n";
453 o << " \"exploitability\": " << t.dread.exploitability << ",\n";
454 o << " \"affected_users\": " << t.dread.affected_users << ",\n";
455 o << " \"discoverability\": " << t.dread.discoverability << ",\n";
456 o << " \"composite\": " << t.dread.composite() << "\n";
457 o << " },\n";
458 o << " \"status\": \"" << mitigation_status_name(t.overall_status()) << "\",\n";
459 o << " \"mitigations\": [";
460 for (size_t j = 0; j < t.mitigations.size(); ++j) {
461 const auto& m = t.mitigations[j];
462 o << "\n {\"id\": \"" << escape_json(m.control_id)
463 << "\", \"status\": \"" << mitigation_status_name(m.status) << "\"}";
464 if (j + 1 < t.mitigations.size()) o << ",";
465 }
466 o << (t.mitigations.empty() ? "" : "\n ") << "]\n";
467 o << " }";
468 if (i + 1 < model.threats.size()) o << ",";
469 o << "\n";
470 }
471
472 o << " ]\n";
473 o << "}";
474 return o.str();
475 }
476};
477
478} // namespace signet::forge
Validates threat model coverage and produces audit-ready JSON.
static ThreatModelAnalysis analyze(const ThreatModel &model)
Analyze a threat model for completeness and risk posture.
static ThreatModel signet_default_model()
Build the Signet Forge default threat model with known threats.
ThreatSeverity
Threat severity classification per NIST SP 800-30.
@ LOW
DREAD composite < 4.0.
@ CRITICAL
DREAD composite >= 9.0.
@ HIGH
DREAD composite 7.0 - 8.9.
@ MEDIUM
DREAD composite 4.0 - 6.9.
@ LOW
Minor documentation update.
@ CRITICAL
Immediate action required (compliance deadline)
@ HIGH
Significant architectural changes.
@ MEDIUM
Code/configuration changes required.
StrideCategory
Microsoft STRIDE threat categories.
@ SPOOFING
Authentication bypass, identity impersonation.
@ ELEVATION_OF_PRIVILEGE
Gaining unauthorized access levels.
@ REPUDIATION
Denying actions without proof.
@ TAMPERING
Unauthorized data modification.
@ DENIAL_OF_SERVICE
Resource exhaustion, availability attacks.
@ INFORMATION_DISCLOSURE
Unauthorized data exposure.
MitigationStatus
Mitigation status for a threat.
@ MITIGATED
Fully mitigated by implemented controls.
@ TRANSFERRED
Risk transferred (insurance, third-party)
@ NOT_MITIGATED
No mitigation in place.
@ ACCEPTED
Risk accepted per organizational policy.
@ PARTIAL
Some controls, residual risk remains.
DREAD risk quantification — 5 factors scored 1..10.
bool valid() const
Validate all factors are in range [1, 10].
double composite() const
Composite DREAD score (arithmetic mean, 1.0 .. 10.0).
int32_t affected_users
Fraction of users affected (1-10)
ThreatSeverity severity() const
Derive severity from composite score.
int32_t discoverability
Ease of discovering the vulnerability (1-10)
int32_t exploitability
Effort required to exploit (1-10)
int32_t damage
Potential damage if exploited (1-10)
int32_t reproducibility
Ease of reproducing the attack (1-10)
A specific mitigation control for a threat.
std::string description
What the control does.
std::string control_id
Unique identifier (e.g., "CTRL-AES-001")
std::string implementation
Where in codebase (file:line or module)
A single identified threat in the threat model.
std::string affected_component
Module or subsystem at risk.
std::vector< std::string > references
CVEs, NIST refs, etc.
std::string title
Short description.
MitigationStatus overall_status() const
Overall mitigation status — worst (lowest) across all mitigations.
std::string attack_vector
How the attack is carried out.
std::string threat_id
Unique identifier (e.g., "T-CRYPT-001")
std::string description
Detailed threat narrative.
DreadScore dread
Risk quantification.
std::vector< Mitigation > mitigations
Analysis result from validating a threat model.
std::string report_json
Full JSON report.
std::vector< StrideCategory > missing_categories
bool stride_complete
All 6 STRIDE categories covered.
A threat model for a specific component or the entire system.
std::string author
Who created/reviewed the model.
std::vector< ThreatEntry > threats
std::string model_id
Unique identifier for this threat model.
std::string version
Version of the threat model.
std::string reviewed_at
ISO 8601 last review timestamp.
std::string component
Component being modeled (e.g., "crypto", "pme")
std::string created_at
ISO 8601 creation timestamp.