Signet Forge 0.1.1
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 expected<ThreatModelAnalysis> analyze(const ThreatModel& model) {
191 auto gate = commercial::require_feature("ThreatModelAnalyzer");
192 if (!gate) return gate.error();
193 ThreatModelAnalysis result;
194 result.total_threats = static_cast<int32_t>(model.threats.size());
195
196 // Track STRIDE coverage
197 bool covered[6] = {};
198 double dread_sum = 0.0;
199 int32_t valid_count = 0;
200
201 for (const auto& t : model.threats) {
202 // Validate DREAD
203 if (!t.dread.valid()) continue;
204
205 ++valid_count;
206 int cat = static_cast<int32_t>(t.category);
207 if (cat >= 0 && cat < 6) covered[cat] = true;
208
209 auto sev = t.dread.severity();
210 switch (sev) {
211 case ThreatSeverity::CRITICAL: ++result.critical_count; break;
212 case ThreatSeverity::HIGH: ++result.high_count; break;
213 case ThreatSeverity::MEDIUM: ++result.medium_count; break;
214 case ThreatSeverity::LOW: ++result.low_count; break;
215 }
216
217 if (t.overall_status() >= MitigationStatus::MITIGATED)
218 ++result.mitigated_count;
219 else
220 ++result.unmitigated_count;
221
222 dread_sum += t.dread.composite();
223 }
224
225 if (valid_count > 0)
226 result.mean_dread_score = dread_sum / valid_count;
227
228 // Check STRIDE completeness
229 result.stride_complete = true;
230 for (int i = 0; i < 6; ++i) {
231 if (!covered[i]) {
232 result.stride_complete = false;
233 result.missing_categories.push_back(
234 static_cast<StrideCategory>(i));
235 }
236 }
237
238 // Generate JSON report
239 result.report_json = generate_json(model, result);
240 return result;
241 }
242
244 [[nodiscard]] static ThreatModel signet_default_model() {
245 ThreatModel m;
246 m.model_id = "SIGNET-TM-001";
247 m.component = "signet::forge";
248 m.version = "1.0.0";
249 m.author = "Signet Security Team";
250
251 // --- SPOOFING ---
252 m.threats.push_back(ThreatEntry{
253 "T-AUTH-001", "Key impersonation via INTERNAL mode",
254 "An attacker with access to plaintext keys in INTERNAL mode can "
255 "impersonate any column encryption identity.",
257 DreadScore{7, 8, 5, 6, 4},
258 "Access to unencrypted key material in file metadata",
259 "crypto/pme.hpp",
260 {Mitigation{"CTRL-KMS-001",
261 "EXTERNAL key mode with KMS integration",
262 "crypto/key_metadata.hpp:IKmsClient",
264 Mitigation{"CTRL-GATE-001",
265 "Production gate rejects INTERNAL mode (C-15)",
266 "crypto/pme.hpp:production_key_mode_gate()",
268 {"NIST SP 800-57", "PARQUET-1178"}
269 });
270
271 // --- TAMPERING ---
272 m.threats.push_back(ThreatEntry{
273 "T-TAMP-001", "Hash chain manipulation in audit logs",
274 "An attacker modifies audit chain entries without detection.",
276 DreadScore{9, 3, 4, 8, 3},
277 "Direct modification of Parquet audit log files",
278 "ai/audit_chain.hpp",
279 {Mitigation{"CTRL-CHAIN-001",
280 "SHA-256 cryptographic hash chain with prev_hash linkage",
281 "ai/audit_chain.hpp:AuditChainHasher",
283 {"SEC 17a-4", "NIST SP 800-92"}
284 });
285
286 // --- REPUDIATION ---
287 m.threats.push_back(ThreatEntry{
288 "T-REP-001", "Denial of AI decision actions",
289 "An operator denies having made or approved an AI trading decision.",
291 DreadScore{6, 7, 3, 5, 4},
292 "Lack of non-repudiable logging for human overrides",
293 "ai/decision_log.hpp",
294 {Mitigation{"CTRL-LOG-001",
295 "Immutable decision log with operator_id and hash chain",
296 "ai/decision_log.hpp:DecisionLogWriter",
298 Mitigation{"CTRL-OVER-001",
299 "Human override log with provenance (EU AI Act Art.14)",
300 "ai/human_oversight.hpp:HumanOverrideLogWriter",
302 {"EU AI Act Art.14", "MiFID II RTS 24"}
303 });
304
305 // --- INFORMATION DISCLOSURE ---
306 m.threats.push_back(ThreatEntry{
307 "T-DISC-001", "Side-channel leakage from AES timing",
308 "An attacker observes timing variations in AES operations to "
309 "recover key material.",
311 DreadScore{10, 4, 7, 3, 5},
312 "Timing analysis of AES encrypt/decrypt operations",
313 "crypto/aes_core.hpp",
314 {Mitigation{"CTRL-CT-001",
315 "Constant-time AES via bitsliced S-box + AES-NI detection",
316 "crypto/aes_core.hpp:Aes256",
318 Mitigation{"CTRL-ZERO-001",
319 "Key material zeroing in destructors",
320 "crypto/aes_core.hpp:~Aes256()",
322 {"NIST SP 800-38D", "CWE-208"}
323 });
324
325 // --- DENIAL OF SERVICE ---
326 m.threats.push_back(ThreatEntry{
327 "T-DOS-001", "Decompression bomb via crafted Parquet pages",
328 "A malicious Parquet file with extreme compression ratios causes "
329 "memory exhaustion during decompression.",
331 DreadScore{7, 9, 8, 5, 7},
332 "Crafted Parquet file with oversized uncompressed pages",
333 "reader.hpp",
334 {Mitigation{"CTRL-PAGE-001",
335 "PARQUET_MAX_PAGE_SIZE (256 MB) limit on decompressed pages",
336 "reader.hpp:PARQUET_MAX_PAGE_SIZE",
338 Mitigation{"CTRL-THRIFT-001",
339 "Thrift field count (65536) and string size (64 MB) limits",
340 "thrift/compact.hpp:MAX_FIELD_COUNT",
342 {"CWE-409", "OWASP Decompression Bomb"}
343 });
344
345 // --- ELEVATION OF PRIVILEGE ---
346 m.threats.push_back(ThreatEntry{
347 "T-PRIV-001", "Path traversal in FeatureWriter output_dir",
348 "An attacker supplies a path with '..' segments to write outside "
349 "the intended directory.",
351 DreadScore{8, 9, 7, 4, 8},
352 "Controlled output_dir parameter with path traversal sequences",
353 "ai/feature_writer.hpp",
354 {Mitigation{"CTRL-PATH-001",
355 "Path traversal guard rejects '..' segments",
356 "ai/feature_writer.hpp:create()",
358 {"CWE-22", "OWASP Path Traversal"}
359 });
360
361 return m;
362 }
363
364private:
365 static std::string stride_name(StrideCategory c) {
366 switch (c) {
367 case StrideCategory::SPOOFING: return "Spoofing";
368 case StrideCategory::TAMPERING: return "Tampering";
369 case StrideCategory::REPUDIATION: return "Repudiation";
370 case StrideCategory::INFORMATION_DISCLOSURE: return "Information Disclosure";
371 case StrideCategory::DENIAL_OF_SERVICE: return "Denial of Service";
372 case StrideCategory::ELEVATION_OF_PRIVILEGE: return "Elevation of Privilege";
373 }
374 return "Unknown";
375 }
376
377 static std::string severity_name(ThreatSeverity s) {
378 switch (s) {
379 case ThreatSeverity::LOW: return "Low";
380 case ThreatSeverity::MEDIUM: return "Medium";
381 case ThreatSeverity::HIGH: return "High";
382 case ThreatSeverity::CRITICAL: return "Critical";
383 }
384 return "Unknown";
385 }
386
387 static std::string mitigation_status_name(MitigationStatus s) {
388 switch (s) {
389 case MitigationStatus::NOT_MITIGATED: return "Not Mitigated";
390 case MitigationStatus::PARTIAL: return "Partial";
391 case MitigationStatus::MITIGATED: return "Mitigated";
392 case MitigationStatus::ACCEPTED: return "Accepted";
393 case MitigationStatus::TRANSFERRED: return "Transferred";
394 }
395 return "Unknown";
396 }
397
398 static std::string escape_json(const std::string& s) {
399 std::string out;
400 out.reserve(s.size() + 16);
401 for (char c : s) {
402 switch (c) {
403 case '"': out += "\\\""; break;
404 case '\\': out += "\\\\"; break;
405 case '\n': out += "\\n"; break;
406 case '\r': out += "\\r"; break;
407 case '\t': out += "\\t"; break;
408 default:
409 if (static_cast<unsigned char>(c) < 0x20) {
410 char buf[8];
411 std::snprintf(buf, sizeof(buf), "\\u%04x",
412 static_cast<unsigned char>(c));
413 out += buf;
414 } else {
415 out += c;
416 }
417 break;
418 }
419 }
420 return out;
421 }
422
423 static std::string generate_json(const ThreatModel& model,
424 const ThreatModelAnalysis& analysis) {
425 std::ostringstream o;
426 o << "{\n";
427 o << " \"model_id\": \"" << escape_json(model.model_id) << "\",\n";
428 o << " \"component\": \"" << escape_json(model.component) << "\",\n";
429 o << " \"version\": \"" << escape_json(model.version) << "\",\n";
430 o << " \"methodology\": \"STRIDE/DREAD\",\n";
431 o << " \"summary\": {\n";
432 o << " \"total_threats\": " << analysis.total_threats << ",\n";
433 o << " \"critical\": " << analysis.critical_count << ",\n";
434 o << " \"high\": " << analysis.high_count << ",\n";
435 o << " \"medium\": " << analysis.medium_count << ",\n";
436 o << " \"low\": " << analysis.low_count << ",\n";
437 o << " \"mitigated\": " << analysis.mitigated_count << ",\n";
438 o << " \"unmitigated\": " << analysis.unmitigated_count << ",\n";
439 o << " \"stride_complete\": " << (analysis.stride_complete ? "true" : "false") << ",\n";
440 o << " \"mean_dread_score\": " << analysis.mean_dread_score << "\n";
441 o << " },\n";
442 o << " \"threats\": [\n";
443
444 for (size_t i = 0; i < model.threats.size(); ++i) {
445 const auto& t = model.threats[i];
446 o << " {\n";
447 o << " \"id\": \"" << escape_json(t.threat_id) << "\",\n";
448 o << " \"title\": \"" << escape_json(t.title) << "\",\n";
449 o << " \"stride\": \"" << stride_name(t.category) << "\",\n";
450 o << " \"severity\": \"" << severity_name(t.dread.severity()) << "\",\n";
451 o << " \"dread\": {\n";
452 o << " \"damage\": " << t.dread.damage << ",\n";
453 o << " \"reproducibility\": " << t.dread.reproducibility << ",\n";
454 o << " \"exploitability\": " << t.dread.exploitability << ",\n";
455 o << " \"affected_users\": " << t.dread.affected_users << ",\n";
456 o << " \"discoverability\": " << t.dread.discoverability << ",\n";
457 o << " \"composite\": " << t.dread.composite() << "\n";
458 o << " },\n";
459 o << " \"status\": \"" << mitigation_status_name(t.overall_status()) << "\",\n";
460 o << " \"mitigations\": [";
461 for (size_t j = 0; j < t.mitigations.size(); ++j) {
462 const auto& m = t.mitigations[j];
463 o << "\n {\"id\": \"" << escape_json(m.control_id)
464 << "\", \"status\": \"" << mitigation_status_name(m.status) << "\"}";
465 if (j + 1 < t.mitigations.size()) o << ",";
466 }
467 o << (t.mitigations.empty() ? "" : "\n ") << "]\n";
468 o << " }";
469 if (i + 1 < model.threats.size()) o << ",";
470 o << "\n";
471 }
472
473 o << " ]\n";
474 o << "}";
475 return o.str();
476 }
477};
478
479} // namespace signet::forge
Validates threat model coverage and produces audit-ready JSON.
static expected< 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.
A lightweight result type that holds either a success value of type T or an Error.
Definition error.hpp:143
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.