paranoid-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.
- backend/__init__.py +3 -0
- backend/config.py +53 -0
- backend/db/__init__.py +0 -0
- backend/db/crud.py +630 -0
- backend/db/schema.py +218 -0
- backend/db/seed.py +295 -0
- backend/db/vectors.py +323 -0
- backend/export/__init__.py +0 -0
- backend/main.py +90 -0
- backend/models/__init__.py +72 -0
- backend/models/enums.py +98 -0
- backend/models/extended.py +321 -0
- backend/models/state.py +205 -0
- backend/pipeline/__init__.py +19 -0
- backend/pipeline/input_parser.py +436 -0
- backend/pipeline/nodes.py +656 -0
- backend/pipeline/prompts/__init__.py +37 -0
- backend/pipeline/prompts/attack_tree.py +217 -0
- backend/pipeline/prompts/maestro.py +429 -0
- backend/pipeline/prompts/stride.py +510 -0
- backend/pipeline/prompts/test_case.py +115 -0
- backend/pipeline/runner.py +527 -0
- backend/providers/__init__.py +25 -0
- backend/providers/anthropic.py +155 -0
- backend/providers/base.py +214 -0
- backend/providers/ollama.py +168 -0
- backend/providers/openai.py +160 -0
- backend/routes/__init__.py +0 -0
- backend/rules/__init__.py +0 -0
- cli/__init__.py +0 -0
- cli/commands/__init__.py +1 -0
- cli/commands/config.py +304 -0
- cli/commands/run.py +482 -0
- cli/commands/version.py +76 -0
- cli/context.py +160 -0
- cli/errors.py +43 -0
- cli/input/__init__.py +1 -0
- cli/input/file_loader.py +172 -0
- cli/main.py +48 -0
- cli/output/__init__.py +1 -0
- cli/output/console.py +117 -0
- cli/output/json_writer.py +254 -0
- paranoid_cli-1.0.0.dist-info/METADATA +480 -0
- paranoid_cli-1.0.0.dist-info/RECORD +48 -0
- paranoid_cli-1.0.0.dist-info/WHEEL +5 -0
- paranoid_cli-1.0.0.dist-info/entry_points.txt +2 -0
- paranoid_cli-1.0.0.dist-info/licenses/LICENSE +201 -0
- paranoid_cli-1.0.0.dist-info/top_level.txt +2 -0
backend/__init__.py
ADDED
backend/config.py
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"""Application configuration using pydantic-settings."""
|
|
2
|
+
|
|
3
|
+
from typing import Literal
|
|
4
|
+
|
|
5
|
+
from pydantic_settings import BaseSettings, SettingsConfigDict
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class Settings(BaseSettings):
|
|
9
|
+
"""Application settings loaded from environment variables."""
|
|
10
|
+
|
|
11
|
+
model_config = SettingsConfigDict(
|
|
12
|
+
env_file=".env",
|
|
13
|
+
env_file_encoding="utf-8",
|
|
14
|
+
case_sensitive=False,
|
|
15
|
+
extra="ignore",
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
# LLM Provider settings
|
|
19
|
+
anthropic_api_key: str = ""
|
|
20
|
+
openai_api_key: str = ""
|
|
21
|
+
ollama_base_url: str = "http://host.docker.internal:11434"
|
|
22
|
+
default_provider: Literal["anthropic", "openai", "ollama"] = "anthropic"
|
|
23
|
+
default_model: str = "claude-sonnet-4-20250514"
|
|
24
|
+
default_iterations: int = 3
|
|
25
|
+
|
|
26
|
+
# Embedding settings
|
|
27
|
+
embedding_model: str = "BAAI/bge-small-en-v1.5"
|
|
28
|
+
|
|
29
|
+
# Database settings
|
|
30
|
+
db_path: str = "./data/paranoid.db"
|
|
31
|
+
|
|
32
|
+
# Server settings
|
|
33
|
+
host: str = "0.0.0.0"
|
|
34
|
+
port: int = 8000
|
|
35
|
+
log_level: Literal["debug", "info", "warning", "error"] = "info"
|
|
36
|
+
|
|
37
|
+
# Prompt configuration
|
|
38
|
+
summary_max_words: int = 40
|
|
39
|
+
threat_description_min_words: int = 35
|
|
40
|
+
threat_description_max_words: int = 50
|
|
41
|
+
mitigation_min_items: int = 2
|
|
42
|
+
mitigation_max_items: int = 5
|
|
43
|
+
|
|
44
|
+
# Pipeline configuration
|
|
45
|
+
max_iteration_count: int = 15
|
|
46
|
+
min_iteration_count: int = 1
|
|
47
|
+
|
|
48
|
+
# Deduplication threshold for rule engine
|
|
49
|
+
similarity_threshold: float = 0.85
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
# Global settings instance
|
|
53
|
+
settings = Settings()
|
backend/db/__init__.py
ADDED
|
File without changes
|
backend/db/crud.py
ADDED
|
@@ -0,0 +1,630 @@
|
|
|
1
|
+
"""Async CRUD operations for SQLite database."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import logging
|
|
5
|
+
import uuid
|
|
6
|
+
from datetime import UTC, datetime
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
import aiosqlite
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
logger = logging.getLogger(__name__)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def generate_id() -> str:
|
|
16
|
+
"""Generate a UUID for database records."""
|
|
17
|
+
return str(uuid.uuid4())
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def now_iso() -> str:
|
|
21
|
+
"""Get current timestamp in ISO 8601 format."""
|
|
22
|
+
return datetime.now(UTC).isoformat()
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
# Threat Models CRUD
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
async def create_threat_model(
|
|
29
|
+
db_path: str,
|
|
30
|
+
title: str,
|
|
31
|
+
description: str,
|
|
32
|
+
provider: str,
|
|
33
|
+
model: str,
|
|
34
|
+
framework: str = "STRIDE",
|
|
35
|
+
iteration_count: int = 1,
|
|
36
|
+
) -> str:
|
|
37
|
+
"""Create a new threat model."""
|
|
38
|
+
model_id = generate_id()
|
|
39
|
+
now = now_iso()
|
|
40
|
+
|
|
41
|
+
async with aiosqlite.connect(db_path) as db:
|
|
42
|
+
await db.execute("PRAGMA foreign_keys = ON;")
|
|
43
|
+
await db.execute(
|
|
44
|
+
"""
|
|
45
|
+
INSERT INTO threat_models (
|
|
46
|
+
id, title, description, framework, provider, model,
|
|
47
|
+
status, iteration_count, created_at, updated_at
|
|
48
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
49
|
+
""",
|
|
50
|
+
(
|
|
51
|
+
model_id,
|
|
52
|
+
title,
|
|
53
|
+
description,
|
|
54
|
+
framework,
|
|
55
|
+
provider,
|
|
56
|
+
model,
|
|
57
|
+
"pending",
|
|
58
|
+
iteration_count,
|
|
59
|
+
now,
|
|
60
|
+
now,
|
|
61
|
+
),
|
|
62
|
+
)
|
|
63
|
+
await db.commit()
|
|
64
|
+
|
|
65
|
+
logger.info(f"Created threat model {model_id}")
|
|
66
|
+
return model_id
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
async def get_threat_model(db_path: str, model_id: str) -> dict[str, Any] | None:
|
|
70
|
+
"""Get a threat model by ID."""
|
|
71
|
+
async with aiosqlite.connect(db_path) as db:
|
|
72
|
+
db.row_factory = aiosqlite.Row
|
|
73
|
+
async with db.execute(
|
|
74
|
+
"SELECT * FROM threat_models WHERE id = ?", (model_id,)
|
|
75
|
+
) as cursor:
|
|
76
|
+
row = await cursor.fetchone()
|
|
77
|
+
return dict(row) if row else None
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
async def update_threat_model_status(
|
|
81
|
+
db_path: str, model_id: str, status: str
|
|
82
|
+
) -> None:
|
|
83
|
+
"""Update threat model status."""
|
|
84
|
+
async with aiosqlite.connect(db_path) as db:
|
|
85
|
+
await db.execute("PRAGMA foreign_keys = ON;")
|
|
86
|
+
await db.execute(
|
|
87
|
+
"""
|
|
88
|
+
UPDATE threat_models
|
|
89
|
+
SET status = ?, updated_at = ?
|
|
90
|
+
WHERE id = ?
|
|
91
|
+
""",
|
|
92
|
+
(status, now_iso(), model_id),
|
|
93
|
+
)
|
|
94
|
+
await db.commit()
|
|
95
|
+
|
|
96
|
+
logger.info(f"Updated threat model {model_id} status to {status}")
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
async def update_threat_model(
|
|
100
|
+
db_path: str,
|
|
101
|
+
model_id: str,
|
|
102
|
+
title: str | None = None,
|
|
103
|
+
description: str | None = None,
|
|
104
|
+
framework: str | None = None,
|
|
105
|
+
status: str | None = None,
|
|
106
|
+
) -> None:
|
|
107
|
+
"""
|
|
108
|
+
Update threat model details. Only provided fields will be updated.
|
|
109
|
+
|
|
110
|
+
Args:
|
|
111
|
+
db_path: Path to SQLite database
|
|
112
|
+
model_id: ID of threat model to update
|
|
113
|
+
title: Model title
|
|
114
|
+
description: Model description
|
|
115
|
+
framework: Framework (STRIDE, MAESTRO, etc.)
|
|
116
|
+
status: Status (pending, in_progress, completed, failed)
|
|
117
|
+
"""
|
|
118
|
+
update_fields = []
|
|
119
|
+
params = []
|
|
120
|
+
|
|
121
|
+
if title is not None:
|
|
122
|
+
update_fields.append("title = ?")
|
|
123
|
+
params.append(title)
|
|
124
|
+
|
|
125
|
+
if description is not None:
|
|
126
|
+
update_fields.append("description = ?")
|
|
127
|
+
params.append(description)
|
|
128
|
+
|
|
129
|
+
if framework is not None:
|
|
130
|
+
update_fields.append("framework = ?")
|
|
131
|
+
params.append(framework)
|
|
132
|
+
|
|
133
|
+
if status is not None:
|
|
134
|
+
update_fields.append("status = ?")
|
|
135
|
+
params.append(status)
|
|
136
|
+
|
|
137
|
+
# Always update timestamp
|
|
138
|
+
update_fields.append("updated_at = ?")
|
|
139
|
+
params.append(now_iso())
|
|
140
|
+
params.append(model_id)
|
|
141
|
+
|
|
142
|
+
if len(update_fields) == 1:
|
|
143
|
+
logger.warning(f"No fields provided to update for model {model_id}")
|
|
144
|
+
return
|
|
145
|
+
|
|
146
|
+
query = f"UPDATE threat_models SET {', '.join(update_fields)} WHERE id = ?"
|
|
147
|
+
|
|
148
|
+
async with aiosqlite.connect(db_path) as db:
|
|
149
|
+
await db.execute("PRAGMA foreign_keys = ON;")
|
|
150
|
+
await db.execute(query, params)
|
|
151
|
+
await db.commit()
|
|
152
|
+
|
|
153
|
+
logger.info(f"Updated threat model {model_id}")
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
async def list_threat_models(db_path: str, limit: int = 50) -> list[dict[str, Any]]:
|
|
157
|
+
"""List all threat models."""
|
|
158
|
+
async with aiosqlite.connect(db_path) as db:
|
|
159
|
+
db.row_factory = aiosqlite.Row
|
|
160
|
+
async with db.execute(
|
|
161
|
+
"SELECT * FROM threat_models ORDER BY created_at DESC LIMIT ?", (limit,)
|
|
162
|
+
) as cursor:
|
|
163
|
+
rows = await cursor.fetchall()
|
|
164
|
+
return [dict(row) for row in rows]
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
# Threats CRUD
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
async def create_threat(
|
|
171
|
+
db_path: str,
|
|
172
|
+
model_id: str,
|
|
173
|
+
name: str,
|
|
174
|
+
description: str,
|
|
175
|
+
target: str,
|
|
176
|
+
impact: str,
|
|
177
|
+
likelihood: str,
|
|
178
|
+
mitigations: list[str],
|
|
179
|
+
stride_category: str | None = None,
|
|
180
|
+
maestro_category: str | None = None,
|
|
181
|
+
dread_score: float | None = None,
|
|
182
|
+
iteration_number: int = 1,
|
|
183
|
+
) -> str:
|
|
184
|
+
"""Create a new threat."""
|
|
185
|
+
threat_id = generate_id()
|
|
186
|
+
now = now_iso()
|
|
187
|
+
mitigations_json = json.dumps(mitigations)
|
|
188
|
+
|
|
189
|
+
async with aiosqlite.connect(db_path) as db:
|
|
190
|
+
await db.execute("PRAGMA foreign_keys = ON;")
|
|
191
|
+
await db.execute(
|
|
192
|
+
"""
|
|
193
|
+
INSERT INTO threats (
|
|
194
|
+
id, model_id, stride_category, maestro_category, name,
|
|
195
|
+
description, target, impact, likelihood, dread_score,
|
|
196
|
+
mitigations, status, iteration_number, created_at, updated_at
|
|
197
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
198
|
+
""",
|
|
199
|
+
(
|
|
200
|
+
threat_id,
|
|
201
|
+
model_id,
|
|
202
|
+
stride_category,
|
|
203
|
+
maestro_category,
|
|
204
|
+
name,
|
|
205
|
+
description,
|
|
206
|
+
target,
|
|
207
|
+
impact,
|
|
208
|
+
likelihood,
|
|
209
|
+
dread_score,
|
|
210
|
+
mitigations_json,
|
|
211
|
+
"pending",
|
|
212
|
+
iteration_number,
|
|
213
|
+
now,
|
|
214
|
+
now,
|
|
215
|
+
),
|
|
216
|
+
)
|
|
217
|
+
await db.commit()
|
|
218
|
+
|
|
219
|
+
logger.info(f"Created threat {threat_id} for model {model_id}")
|
|
220
|
+
return threat_id
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
async def get_threat(db_path: str, threat_id: str) -> dict[str, Any] | None:
|
|
224
|
+
"""Get a threat by ID."""
|
|
225
|
+
async with aiosqlite.connect(db_path) as db:
|
|
226
|
+
db.row_factory = aiosqlite.Row
|
|
227
|
+
async with db.execute(
|
|
228
|
+
"SELECT * FROM threats WHERE id = ?", (threat_id,)
|
|
229
|
+
) as cursor:
|
|
230
|
+
row = await cursor.fetchone()
|
|
231
|
+
if row:
|
|
232
|
+
threat = dict(row)
|
|
233
|
+
threat["mitigations"] = json.loads(threat["mitigations"])
|
|
234
|
+
return threat
|
|
235
|
+
return None
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
async def list_threats(
|
|
239
|
+
db_path: str, model_id: str, status: str | None = None
|
|
240
|
+
) -> list[dict[str, Any]]:
|
|
241
|
+
"""List threats for a model, optionally filtered by status."""
|
|
242
|
+
async with aiosqlite.connect(db_path) as db:
|
|
243
|
+
db.row_factory = aiosqlite.Row
|
|
244
|
+
|
|
245
|
+
if status:
|
|
246
|
+
query = "SELECT * FROM threats WHERE model_id = ? AND status = ? ORDER BY created_at"
|
|
247
|
+
params = (model_id, status)
|
|
248
|
+
else:
|
|
249
|
+
query = "SELECT * FROM threats WHERE model_id = ? ORDER BY created_at"
|
|
250
|
+
params = (model_id,)
|
|
251
|
+
|
|
252
|
+
async with db.execute(query, params) as cursor:
|
|
253
|
+
rows = await cursor.fetchall()
|
|
254
|
+
threats = []
|
|
255
|
+
for row in rows:
|
|
256
|
+
threat = dict(row)
|
|
257
|
+
threat["mitigations"] = json.loads(threat["mitigations"])
|
|
258
|
+
threats.append(threat)
|
|
259
|
+
return threats
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
async def update_threat_status(
|
|
263
|
+
db_path: str, threat_id: str, status: str
|
|
264
|
+
) -> None:
|
|
265
|
+
"""Update threat status (pending/approved/rejected)."""
|
|
266
|
+
async with aiosqlite.connect(db_path) as db:
|
|
267
|
+
await db.execute("PRAGMA foreign_keys = ON;")
|
|
268
|
+
await db.execute(
|
|
269
|
+
"""
|
|
270
|
+
UPDATE threats
|
|
271
|
+
SET status = ?, updated_at = ?
|
|
272
|
+
WHERE id = ?
|
|
273
|
+
""",
|
|
274
|
+
(status, now_iso(), threat_id),
|
|
275
|
+
)
|
|
276
|
+
await db.commit()
|
|
277
|
+
|
|
278
|
+
logger.info(f"Updated threat {threat_id} status to {status}")
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
async def update_threat(
|
|
282
|
+
db_path: str,
|
|
283
|
+
threat_id: str,
|
|
284
|
+
name: str | None = None,
|
|
285
|
+
description: str | None = None,
|
|
286
|
+
target: str | None = None,
|
|
287
|
+
impact: str | None = None,
|
|
288
|
+
likelihood: str | None = None,
|
|
289
|
+
mitigations: list[str] | None = None,
|
|
290
|
+
stride_category: str | None = None,
|
|
291
|
+
maestro_category: str | None = None,
|
|
292
|
+
dread_damage: int | None = None,
|
|
293
|
+
dread_reproducibility: int | None = None,
|
|
294
|
+
dread_exploitability: int | None = None,
|
|
295
|
+
dread_affected_users: int | None = None,
|
|
296
|
+
dread_discoverability: int | None = None,
|
|
297
|
+
dread_score: float | None = None,
|
|
298
|
+
) -> None:
|
|
299
|
+
"""
|
|
300
|
+
Update threat details. Only provided fields will be updated.
|
|
301
|
+
|
|
302
|
+
Args:
|
|
303
|
+
db_path: Path to SQLite database
|
|
304
|
+
threat_id: ID of threat to update
|
|
305
|
+
name: Threat name
|
|
306
|
+
description: Threat description
|
|
307
|
+
target: Threat target
|
|
308
|
+
impact: Impact assessment
|
|
309
|
+
likelihood: Likelihood assessment
|
|
310
|
+
mitigations: List of mitigation strategies
|
|
311
|
+
stride_category: STRIDE category
|
|
312
|
+
maestro_category: MAESTRO category
|
|
313
|
+
dread_damage: DREAD damage score (0-10)
|
|
314
|
+
dread_reproducibility: DREAD reproducibility score (0-10)
|
|
315
|
+
dread_exploitability: DREAD exploitability score (0-10)
|
|
316
|
+
dread_affected_users: DREAD affected users score (0-10)
|
|
317
|
+
dread_discoverability: DREAD discoverability score (0-10)
|
|
318
|
+
dread_score: Overall DREAD score
|
|
319
|
+
"""
|
|
320
|
+
# Build dynamic UPDATE query for only provided fields
|
|
321
|
+
update_fields = []
|
|
322
|
+
params = []
|
|
323
|
+
|
|
324
|
+
if name is not None:
|
|
325
|
+
update_fields.append("name = ?")
|
|
326
|
+
params.append(name)
|
|
327
|
+
|
|
328
|
+
if description is not None:
|
|
329
|
+
update_fields.append("description = ?")
|
|
330
|
+
params.append(description)
|
|
331
|
+
|
|
332
|
+
if target is not None:
|
|
333
|
+
update_fields.append("target = ?")
|
|
334
|
+
params.append(target)
|
|
335
|
+
|
|
336
|
+
if impact is not None:
|
|
337
|
+
update_fields.append("impact = ?")
|
|
338
|
+
params.append(impact)
|
|
339
|
+
|
|
340
|
+
if likelihood is not None:
|
|
341
|
+
update_fields.append("likelihood = ?")
|
|
342
|
+
params.append(likelihood)
|
|
343
|
+
|
|
344
|
+
if mitigations is not None:
|
|
345
|
+
update_fields.append("mitigations = ?")
|
|
346
|
+
params.append(json.dumps(mitigations))
|
|
347
|
+
|
|
348
|
+
if stride_category is not None:
|
|
349
|
+
update_fields.append("stride_category = ?")
|
|
350
|
+
params.append(stride_category)
|
|
351
|
+
|
|
352
|
+
if maestro_category is not None:
|
|
353
|
+
update_fields.append("maestro_category = ?")
|
|
354
|
+
params.append(maestro_category)
|
|
355
|
+
|
|
356
|
+
if dread_damage is not None:
|
|
357
|
+
update_fields.append("dread_damage = ?")
|
|
358
|
+
params.append(dread_damage)
|
|
359
|
+
|
|
360
|
+
if dread_reproducibility is not None:
|
|
361
|
+
update_fields.append("dread_reproducibility = ?")
|
|
362
|
+
params.append(dread_reproducibility)
|
|
363
|
+
|
|
364
|
+
if dread_exploitability is not None:
|
|
365
|
+
update_fields.append("dread_exploitability = ?")
|
|
366
|
+
params.append(dread_exploitability)
|
|
367
|
+
|
|
368
|
+
if dread_affected_users is not None:
|
|
369
|
+
update_fields.append("dread_affected_users = ?")
|
|
370
|
+
params.append(dread_affected_users)
|
|
371
|
+
|
|
372
|
+
if dread_discoverability is not None:
|
|
373
|
+
update_fields.append("dread_discoverability = ?")
|
|
374
|
+
params.append(dread_discoverability)
|
|
375
|
+
|
|
376
|
+
if dread_score is not None:
|
|
377
|
+
update_fields.append("dread_score = ?")
|
|
378
|
+
params.append(dread_score)
|
|
379
|
+
|
|
380
|
+
# Always update the updated_at timestamp
|
|
381
|
+
update_fields.append("updated_at = ?")
|
|
382
|
+
params.append(now_iso())
|
|
383
|
+
|
|
384
|
+
# Add threat_id as final parameter
|
|
385
|
+
params.append(threat_id)
|
|
386
|
+
|
|
387
|
+
if len(update_fields) == 1: # Only updated_at, nothing to update
|
|
388
|
+
logger.warning(f"No fields provided to update for threat {threat_id}")
|
|
389
|
+
return
|
|
390
|
+
|
|
391
|
+
query = f"UPDATE threats SET {', '.join(update_fields)} WHERE id = ?"
|
|
392
|
+
|
|
393
|
+
async with aiosqlite.connect(db_path) as db:
|
|
394
|
+
await db.execute("PRAGMA foreign_keys = ON;")
|
|
395
|
+
await db.execute(query, params)
|
|
396
|
+
await db.commit()
|
|
397
|
+
|
|
398
|
+
logger.info(f"Updated threat {threat_id} with {len(update_fields)} fields")
|
|
399
|
+
|
|
400
|
+
|
|
401
|
+
# Assets CRUD
|
|
402
|
+
|
|
403
|
+
|
|
404
|
+
async def create_asset(
|
|
405
|
+
db_path: str, model_id: str, asset_type: str, name: str, description: str
|
|
406
|
+
) -> str:
|
|
407
|
+
"""Create a new asset."""
|
|
408
|
+
asset_id = generate_id()
|
|
409
|
+
now = now_iso()
|
|
410
|
+
|
|
411
|
+
async with aiosqlite.connect(db_path) as db:
|
|
412
|
+
await db.execute("PRAGMA foreign_keys = ON;")
|
|
413
|
+
await db.execute(
|
|
414
|
+
"""
|
|
415
|
+
INSERT INTO assets (id, model_id, type, name, description, created_at)
|
|
416
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
417
|
+
""",
|
|
418
|
+
(asset_id, model_id, asset_type, name, description, now),
|
|
419
|
+
)
|
|
420
|
+
await db.commit()
|
|
421
|
+
|
|
422
|
+
return asset_id
|
|
423
|
+
|
|
424
|
+
|
|
425
|
+
async def list_assets(db_path: str, model_id: str) -> list[dict[str, Any]]:
|
|
426
|
+
"""List all assets for a model."""
|
|
427
|
+
async with aiosqlite.connect(db_path) as db:
|
|
428
|
+
db.row_factory = aiosqlite.Row
|
|
429
|
+
async with db.execute(
|
|
430
|
+
"SELECT * FROM assets WHERE model_id = ? ORDER BY created_at", (model_id,)
|
|
431
|
+
) as cursor:
|
|
432
|
+
rows = await cursor.fetchall()
|
|
433
|
+
return [dict(row) for row in rows]
|
|
434
|
+
|
|
435
|
+
|
|
436
|
+
async def update_asset(
|
|
437
|
+
db_path: str,
|
|
438
|
+
asset_id: str,
|
|
439
|
+
name: str | None = None,
|
|
440
|
+
description: str | None = None,
|
|
441
|
+
asset_type: str | None = None,
|
|
442
|
+
) -> None:
|
|
443
|
+
"""
|
|
444
|
+
Update asset details. Only provided fields will be updated.
|
|
445
|
+
|
|
446
|
+
Args:
|
|
447
|
+
db_path: Path to SQLite database
|
|
448
|
+
asset_id: ID of asset to update
|
|
449
|
+
name: Asset name
|
|
450
|
+
description: Asset description
|
|
451
|
+
asset_type: Asset type (Asset/Entity)
|
|
452
|
+
"""
|
|
453
|
+
update_fields = []
|
|
454
|
+
params = []
|
|
455
|
+
|
|
456
|
+
if name is not None:
|
|
457
|
+
update_fields.append("name = ?")
|
|
458
|
+
params.append(name)
|
|
459
|
+
|
|
460
|
+
if description is not None:
|
|
461
|
+
update_fields.append("description = ?")
|
|
462
|
+
params.append(description)
|
|
463
|
+
|
|
464
|
+
if asset_type is not None:
|
|
465
|
+
update_fields.append("type = ?")
|
|
466
|
+
params.append(asset_type)
|
|
467
|
+
|
|
468
|
+
params.append(asset_id)
|
|
469
|
+
|
|
470
|
+
if not update_fields:
|
|
471
|
+
logger.warning(f"No fields provided to update for asset {asset_id}")
|
|
472
|
+
return
|
|
473
|
+
|
|
474
|
+
query = f"UPDATE assets SET {', '.join(update_fields)} WHERE id = ?"
|
|
475
|
+
|
|
476
|
+
async with aiosqlite.connect(db_path) as db:
|
|
477
|
+
await db.execute("PRAGMA foreign_keys = ON;")
|
|
478
|
+
await db.execute(query, params)
|
|
479
|
+
await db.commit()
|
|
480
|
+
|
|
481
|
+
logger.info(f"Updated asset {asset_id}")
|
|
482
|
+
|
|
483
|
+
|
|
484
|
+
async def delete_threat(db_path: str, threat_id: str) -> None:
|
|
485
|
+
"""
|
|
486
|
+
Delete a threat and its associated data.
|
|
487
|
+
|
|
488
|
+
Args:
|
|
489
|
+
db_path: Path to SQLite database
|
|
490
|
+
threat_id: ID of threat to delete
|
|
491
|
+
"""
|
|
492
|
+
async with aiosqlite.connect(db_path) as db:
|
|
493
|
+
await db.execute("PRAGMA foreign_keys = ON;")
|
|
494
|
+
|
|
495
|
+
# Delete threat (CASCADE will handle related records)
|
|
496
|
+
await db.execute("DELETE FROM threats WHERE id = ?", (threat_id,))
|
|
497
|
+
await db.commit()
|
|
498
|
+
|
|
499
|
+
logger.info(f"Deleted threat {threat_id}")
|
|
500
|
+
|
|
501
|
+
|
|
502
|
+
async def delete_threat_model(db_path: str, model_id: str) -> None:
|
|
503
|
+
"""
|
|
504
|
+
Delete a threat model and all associated data.
|
|
505
|
+
|
|
506
|
+
Args:
|
|
507
|
+
db_path: Path to SQLite database
|
|
508
|
+
model_id: ID of threat model to delete
|
|
509
|
+
"""
|
|
510
|
+
async with aiosqlite.connect(db_path) as db:
|
|
511
|
+
await db.execute("PRAGMA foreign_keys = ON;")
|
|
512
|
+
|
|
513
|
+
# Delete model (CASCADE will handle all related records)
|
|
514
|
+
await db.execute("DELETE FROM threat_models WHERE id = ?", (model_id,))
|
|
515
|
+
await db.commit()
|
|
516
|
+
|
|
517
|
+
logger.info(f"Deleted threat model {model_id}")
|
|
518
|
+
|
|
519
|
+
|
|
520
|
+
# Flows CRUD
|
|
521
|
+
|
|
522
|
+
|
|
523
|
+
async def create_flow(
|
|
524
|
+
db_path: str,
|
|
525
|
+
model_id: str,
|
|
526
|
+
flow_type: str,
|
|
527
|
+
flow_description: str,
|
|
528
|
+
source_entity: str,
|
|
529
|
+
target_entity: str,
|
|
530
|
+
) -> str:
|
|
531
|
+
"""Create a new data flow."""
|
|
532
|
+
flow_id = generate_id()
|
|
533
|
+
now = now_iso()
|
|
534
|
+
|
|
535
|
+
async with aiosqlite.connect(db_path) as db:
|
|
536
|
+
await db.execute("PRAGMA foreign_keys = ON;")
|
|
537
|
+
await db.execute(
|
|
538
|
+
"""
|
|
539
|
+
INSERT INTO flows (
|
|
540
|
+
id, model_id, flow_type, flow_description,
|
|
541
|
+
source_entity, target_entity, created_at
|
|
542
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
543
|
+
""",
|
|
544
|
+
(flow_id, model_id, flow_type, flow_description, source_entity, target_entity, now),
|
|
545
|
+
)
|
|
546
|
+
await db.commit()
|
|
547
|
+
|
|
548
|
+
return flow_id
|
|
549
|
+
|
|
550
|
+
|
|
551
|
+
async def list_flows(db_path: str, model_id: str) -> list[dict[str, Any]]:
|
|
552
|
+
"""List all data flows for a model."""
|
|
553
|
+
async with aiosqlite.connect(db_path) as db:
|
|
554
|
+
db.row_factory = aiosqlite.Row
|
|
555
|
+
async with db.execute(
|
|
556
|
+
"SELECT * FROM flows WHERE model_id = ? ORDER BY created_at", (model_id,)
|
|
557
|
+
) as cursor:
|
|
558
|
+
rows = await cursor.fetchall()
|
|
559
|
+
return [dict(row) for row in rows]
|
|
560
|
+
|
|
561
|
+
|
|
562
|
+
# Pipeline Runs CRUD
|
|
563
|
+
|
|
564
|
+
|
|
565
|
+
async def create_pipeline_run(
|
|
566
|
+
db_path: str,
|
|
567
|
+
model_id: str,
|
|
568
|
+
iteration: int,
|
|
569
|
+
step: str,
|
|
570
|
+
input_hash: str,
|
|
571
|
+
output_hash: str,
|
|
572
|
+
provider: str,
|
|
573
|
+
duration_ms: int,
|
|
574
|
+
tokens_used: int | None = None,
|
|
575
|
+
) -> str:
|
|
576
|
+
"""Create a pipeline run audit record."""
|
|
577
|
+
run_id = generate_id()
|
|
578
|
+
now = now_iso()
|
|
579
|
+
|
|
580
|
+
async with aiosqlite.connect(db_path) as db:
|
|
581
|
+
await db.execute("PRAGMA foreign_keys = ON;")
|
|
582
|
+
await db.execute(
|
|
583
|
+
"""
|
|
584
|
+
INSERT INTO pipeline_runs (
|
|
585
|
+
id, model_id, iteration, step, input_hash, output_hash,
|
|
586
|
+
provider, tokens_used, duration_ms, created_at
|
|
587
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
588
|
+
""",
|
|
589
|
+
(
|
|
590
|
+
run_id,
|
|
591
|
+
model_id,
|
|
592
|
+
iteration,
|
|
593
|
+
step,
|
|
594
|
+
input_hash,
|
|
595
|
+
output_hash,
|
|
596
|
+
provider,
|
|
597
|
+
tokens_used,
|
|
598
|
+
duration_ms,
|
|
599
|
+
now,
|
|
600
|
+
),
|
|
601
|
+
)
|
|
602
|
+
await db.commit()
|
|
603
|
+
|
|
604
|
+
return run_id
|
|
605
|
+
|
|
606
|
+
|
|
607
|
+
async def get_pipeline_stats(db_path: str, model_id: str) -> dict[str, Any]:
|
|
608
|
+
"""Get pipeline execution statistics for a model."""
|
|
609
|
+
async with aiosqlite.connect(db_path) as db:
|
|
610
|
+
async with db.execute(
|
|
611
|
+
"""
|
|
612
|
+
SELECT
|
|
613
|
+
COUNT(*) as total_steps,
|
|
614
|
+
SUM(duration_ms) as total_duration_ms,
|
|
615
|
+
SUM(tokens_used) as total_tokens,
|
|
616
|
+
AVG(duration_ms) as avg_duration_ms
|
|
617
|
+
FROM pipeline_runs
|
|
618
|
+
WHERE model_id = ?
|
|
619
|
+
""",
|
|
620
|
+
(model_id,),
|
|
621
|
+
) as cursor:
|
|
622
|
+
row = await cursor.fetchone()
|
|
623
|
+
if row:
|
|
624
|
+
return {
|
|
625
|
+
"total_steps": row[0],
|
|
626
|
+
"total_duration_ms": row[1],
|
|
627
|
+
"total_tokens": row[2],
|
|
628
|
+
"avg_duration_ms": row[3],
|
|
629
|
+
}
|
|
630
|
+
return {}
|