secgraphai 1.0.0rc1__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.
- secgraphai/__init__.py +102 -0
- secgraphai/adapters.py +289 -0
- secgraphai/attacks.py +375 -0
- secgraphai/canary.py +41 -0
- secgraphai/cli.py +778 -0
- secgraphai/config.py +102 -0
- secgraphai/core.py +151 -0
- secgraphai/dashboard.py +475 -0
- secgraphai/discovery.py +465 -0
- secgraphai/evaluation.py +117 -0
- secgraphai/graph.py +191 -0
- secgraphai/instrumentation.py +178 -0
- secgraphai/intelligence.py +786 -0
- secgraphai/invariants.py +180 -0
- secgraphai/lifecycle.py +414 -0
- secgraphai/model.py +76 -0
- secgraphai/multimodal.py +38 -0
- secgraphai/packs/__init__.py +1 -0
- secgraphai/packs/official_ed25519.pub +1 -0
- secgraphai/packs/prompt_injection_core.yaml +47 -0
- secgraphai/plugins.py +201 -0
- secgraphai/policy.py +144 -0
- secgraphai/provenance.py +73 -0
- secgraphai/reporting.py +184 -0
- secgraphai/research.py +61 -0
- secgraphai/roles.py +130 -0
- secgraphai/runtime.py +304 -0
- secgraphai/scanner.py +593 -0
- secgraphai/schemas.py +140 -0
- secgraphai/security.py +211 -0
- secgraphai/security_modules.py +897 -0
- secgraphai/self_security.py +212 -0
- secgraphai/static/app.js +11 -0
- secgraphai/static/index.html +43 -0
- secgraphai/static/styles.css +9 -0
- secgraphai/storage.py +260 -0
- secgraphai/supply_chain.py +64 -0
- secgraphai/targets.py +368 -0
- secgraphai/validators.py +284 -0
- secgraphai-1.0.0rc1.dist-info/METADATA +208 -0
- secgraphai-1.0.0rc1.dist-info/RECORD +44 -0
- secgraphai-1.0.0rc1.dist-info/WHEEL +4 -0
- secgraphai-1.0.0rc1.dist-info/entry_points.txt +2 -0
- secgraphai-1.0.0rc1.dist-info/licenses/LICENSE +18 -0
secgraphai/__init__.py
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
"""Public API for SecGraphAI."""
|
|
2
|
+
|
|
3
|
+
from secgraphai.attacks import (
|
|
4
|
+
Attack,
|
|
5
|
+
AttackMapping,
|
|
6
|
+
AttackMemory,
|
|
7
|
+
AttackPack,
|
|
8
|
+
AttackPlanner,
|
|
9
|
+
PackTrust,
|
|
10
|
+
load_official_pack,
|
|
11
|
+
)
|
|
12
|
+
from secgraphai.canary import CanaryFactory
|
|
13
|
+
from secgraphai.config import Config, Mode
|
|
14
|
+
from secgraphai.core import (
|
|
15
|
+
Evidence,
|
|
16
|
+
Finding,
|
|
17
|
+
Interaction,
|
|
18
|
+
Report,
|
|
19
|
+
ScanManifest,
|
|
20
|
+
Severity,
|
|
21
|
+
Verdict,
|
|
22
|
+
Verification,
|
|
23
|
+
)
|
|
24
|
+
from secgraphai.discovery import (
|
|
25
|
+
DiscoveredComponent,
|
|
26
|
+
DiscoveryInventory,
|
|
27
|
+
discover_fastapi,
|
|
28
|
+
discover_mcp,
|
|
29
|
+
discover_path,
|
|
30
|
+
discover_url,
|
|
31
|
+
)
|
|
32
|
+
from secgraphai.evaluation import (
|
|
33
|
+
DifferentialResult,
|
|
34
|
+
ResourceAmplification,
|
|
35
|
+
ResourceUsage,
|
|
36
|
+
UtilityResult,
|
|
37
|
+
compare_utility,
|
|
38
|
+
detect_amplification,
|
|
39
|
+
measure,
|
|
40
|
+
)
|
|
41
|
+
from secgraphai.graph import EdgeType, NodeType, SecurityGraph
|
|
42
|
+
from secgraphai.invariants import Invariant, InvariantEngine, invariant
|
|
43
|
+
from secgraphai.model import Model
|
|
44
|
+
from secgraphai.policy import PolicyEngine, Rule
|
|
45
|
+
from secgraphai.provenance import Confidence, Label, Provenance, Tainted
|
|
46
|
+
from secgraphai.roles import ModelRole, ModelRoles
|
|
47
|
+
from secgraphai.runtime import Sensitive, secgraph
|
|
48
|
+
from secgraphai.scanner import SecGraph
|
|
49
|
+
|
|
50
|
+
canary = CanaryFactory()
|
|
51
|
+
|
|
52
|
+
__all__ = [
|
|
53
|
+
"Attack",
|
|
54
|
+
"AttackMemory",
|
|
55
|
+
"AttackMapping",
|
|
56
|
+
"AttackPack",
|
|
57
|
+
"AttackPlanner",
|
|
58
|
+
"Config",
|
|
59
|
+
"Confidence",
|
|
60
|
+
"DiscoveredComponent",
|
|
61
|
+
"DiscoveryInventory",
|
|
62
|
+
"DifferentialResult",
|
|
63
|
+
"EdgeType",
|
|
64
|
+
"Evidence",
|
|
65
|
+
"Finding",
|
|
66
|
+
"Interaction",
|
|
67
|
+
"Invariant",
|
|
68
|
+
"InvariantEngine",
|
|
69
|
+
"Label",
|
|
70
|
+
"Mode",
|
|
71
|
+
"Model",
|
|
72
|
+
"ModelRole",
|
|
73
|
+
"ModelRoles",
|
|
74
|
+
"NodeType",
|
|
75
|
+
"Report",
|
|
76
|
+
"ResourceAmplification",
|
|
77
|
+
"ResourceUsage",
|
|
78
|
+
"SecGraph",
|
|
79
|
+
"SecurityGraph",
|
|
80
|
+
"Sensitive",
|
|
81
|
+
"Severity",
|
|
82
|
+
"PackTrust",
|
|
83
|
+
"PolicyEngine",
|
|
84
|
+
"Provenance",
|
|
85
|
+
"Rule",
|
|
86
|
+
"ScanManifest",
|
|
87
|
+
"Tainted",
|
|
88
|
+
"UtilityResult",
|
|
89
|
+
"Verdict",
|
|
90
|
+
"Verification",
|
|
91
|
+
"canary",
|
|
92
|
+
"compare_utility",
|
|
93
|
+
"detect_amplification",
|
|
94
|
+
"discover_mcp",
|
|
95
|
+
"discover_fastapi",
|
|
96
|
+
"discover_path",
|
|
97
|
+
"discover_url",
|
|
98
|
+
"invariant",
|
|
99
|
+
"load_official_pack",
|
|
100
|
+
"measure",
|
|
101
|
+
"secgraph",
|
|
102
|
+
]
|
secgraphai/adapters.py
ADDED
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
"""Permission-gated adapters for external security engines."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Iterable
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from secgraphai.core import Evidence, Finding, Severity, Verdict
|
|
10
|
+
from secgraphai.plugins import PluginManifest, run_plugin
|
|
11
|
+
|
|
12
|
+
SUPPORTED_ENGINES = frozenset(
|
|
13
|
+
{
|
|
14
|
+
"pyrit",
|
|
15
|
+
"garak",
|
|
16
|
+
"promptfoo",
|
|
17
|
+
"deepteam",
|
|
18
|
+
"schemathesis",
|
|
19
|
+
"llmguard",
|
|
20
|
+
"presidio",
|
|
21
|
+
"semgrep",
|
|
22
|
+
"codeql",
|
|
23
|
+
"zap",
|
|
24
|
+
"nuclei",
|
|
25
|
+
"detect-secrets",
|
|
26
|
+
"pip-audit",
|
|
27
|
+
"osv",
|
|
28
|
+
"modelscan",
|
|
29
|
+
}
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
ENGINE_CAPABILITIES = {
|
|
33
|
+
"pyrit": "prompt-injection",
|
|
34
|
+
"garak": "model-probes",
|
|
35
|
+
"promptfoo": "prompt-evaluation",
|
|
36
|
+
"deepteam": "red-team",
|
|
37
|
+
"schemathesis": "api-property-testing",
|
|
38
|
+
"llmguard": "content-filtering",
|
|
39
|
+
"presidio": "pii-detection",
|
|
40
|
+
"semgrep": "static-analysis",
|
|
41
|
+
"codeql": "static-analysis",
|
|
42
|
+
"zap": "dynamic-api-testing",
|
|
43
|
+
"nuclei": "template-scanning",
|
|
44
|
+
"detect-secrets": "secret-detection",
|
|
45
|
+
"pip-audit": "dependency-vulnerabilities",
|
|
46
|
+
"osv": "dependency-vulnerabilities",
|
|
47
|
+
"modelscan": "model-supply-chain",
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@dataclass(frozen=True)
|
|
52
|
+
class Adapter:
|
|
53
|
+
name: str
|
|
54
|
+
manifest: PluginManifest
|
|
55
|
+
|
|
56
|
+
def __post_init__(self) -> None:
|
|
57
|
+
if self.name.casefold() not in SUPPORTED_ENGINES:
|
|
58
|
+
raise ValueError(f"unsupported security engine: {self.name}")
|
|
59
|
+
|
|
60
|
+
def run(
|
|
61
|
+
self, target: dict[str, Any], *, allowed_permissions: Iterable[str] = ()
|
|
62
|
+
) -> list[Finding]:
|
|
63
|
+
output = run_plugin(
|
|
64
|
+
self.manifest, {"target": target}, allowed_permissions=allowed_permissions
|
|
65
|
+
)
|
|
66
|
+
records = normalize_engine_output(self.name, output)
|
|
67
|
+
findings = []
|
|
68
|
+
for index, record in enumerate(records):
|
|
69
|
+
if not isinstance(record, dict):
|
|
70
|
+
continue
|
|
71
|
+
mappings = record.get("mappings", {})
|
|
72
|
+
safe_mappings = (
|
|
73
|
+
{
|
|
74
|
+
str(key): [str(item) for item in value]
|
|
75
|
+
for key, value in mappings.items()
|
|
76
|
+
if isinstance(value, list)
|
|
77
|
+
}
|
|
78
|
+
if isinstance(mappings, dict)
|
|
79
|
+
else {}
|
|
80
|
+
)
|
|
81
|
+
findings.append(
|
|
82
|
+
Finding(
|
|
83
|
+
id=str(record.get("id", f"{self.name}-{index}"))[:200],
|
|
84
|
+
title=str(record.get("title", "External engine finding"))[:4_000],
|
|
85
|
+
severity=Severity(_severity(record.get("severity"))),
|
|
86
|
+
verdict=_verdict(record.get("verdict")),
|
|
87
|
+
confidence=_confidence(record.get("confidence")),
|
|
88
|
+
evidence=[
|
|
89
|
+
Evidence(
|
|
90
|
+
kind=f"adapter:{self.name}",
|
|
91
|
+
description="Normalized external security-engine result",
|
|
92
|
+
metadata={"engine": self.name, "raw_id": record.get("id")},
|
|
93
|
+
)
|
|
94
|
+
],
|
|
95
|
+
mappings=safe_mappings,
|
|
96
|
+
)
|
|
97
|
+
)
|
|
98
|
+
return findings
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def normalize_engine_output(name: str, output: dict[str, object]) -> list[dict[str, Any]]:
|
|
102
|
+
"""Normalize native JSON from supported engines without executing or fetching references."""
|
|
103
|
+
direct = output.get("findings")
|
|
104
|
+
if isinstance(direct, list):
|
|
105
|
+
return [item for item in direct if isinstance(item, dict)]
|
|
106
|
+
normalized = name.casefold()
|
|
107
|
+
if normalized == "semgrep":
|
|
108
|
+
return [
|
|
109
|
+
{
|
|
110
|
+
"id": item.get("check_id"),
|
|
111
|
+
"title": item.get("extra", {}).get("message", "Semgrep finding"),
|
|
112
|
+
"severity": _severity(item.get("extra", {}).get("severity")),
|
|
113
|
+
"mappings": {"CWE": item.get("extra", {}).get("metadata", {}).get("cwe", [])},
|
|
114
|
+
}
|
|
115
|
+
for item in _records(output.get("results"))
|
|
116
|
+
if isinstance(item, dict) and isinstance(item.get("extra"), dict)
|
|
117
|
+
]
|
|
118
|
+
if normalized in {"codeql", "zap"}:
|
|
119
|
+
sarif = _sarif_records(output)
|
|
120
|
+
if sarif:
|
|
121
|
+
return sarif
|
|
122
|
+
if normalized == "zap":
|
|
123
|
+
return [
|
|
124
|
+
{
|
|
125
|
+
"id": alert.get("pluginid"),
|
|
126
|
+
"title": alert.get("alert", "ZAP finding"),
|
|
127
|
+
"severity": {"3": "HIGH", "2": "MEDIUM", "1": "LOW"}.get(
|
|
128
|
+
str(alert.get("riskcode")), "INFO"
|
|
129
|
+
),
|
|
130
|
+
}
|
|
131
|
+
for site in _records(output.get("site"))
|
|
132
|
+
for alert in _records(site.get("alerts"))
|
|
133
|
+
]
|
|
134
|
+
if normalized == "nuclei":
|
|
135
|
+
values = output.get("results", output.get("templates", []))
|
|
136
|
+
return [
|
|
137
|
+
{
|
|
138
|
+
"id": item.get("template-id"),
|
|
139
|
+
"title": item.get("info", {}).get("name", "Nuclei finding"),
|
|
140
|
+
"severity": _severity(item.get("info", {}).get("severity")),
|
|
141
|
+
}
|
|
142
|
+
for item in _records(values)
|
|
143
|
+
if isinstance(item.get("info"), dict)
|
|
144
|
+
]
|
|
145
|
+
if normalized == "pip-audit":
|
|
146
|
+
dependencies = output.get("dependencies", [])
|
|
147
|
+
return [
|
|
148
|
+
{
|
|
149
|
+
"id": vulnerability.get("id"),
|
|
150
|
+
"title": f"{dependency.get('name')} {dependency.get('version')} is vulnerable",
|
|
151
|
+
"severity": "HIGH",
|
|
152
|
+
"mappings": {"aliases": vulnerability.get("aliases", [])},
|
|
153
|
+
}
|
|
154
|
+
for dependency in _records(dependencies)
|
|
155
|
+
for vulnerability in _records(dependency.get("vulns"))
|
|
156
|
+
]
|
|
157
|
+
if normalized == "detect-secrets":
|
|
158
|
+
results = output.get("results", {})
|
|
159
|
+
return [
|
|
160
|
+
{
|
|
161
|
+
"id": f"{path}:{item.get('line_number', index)}",
|
|
162
|
+
"title": str(item.get("type", "Potential secret")),
|
|
163
|
+
"severity": "HIGH",
|
|
164
|
+
}
|
|
165
|
+
for path, values in (results.items() if isinstance(results, dict) else ())
|
|
166
|
+
if isinstance(path, str)
|
|
167
|
+
for index, item in enumerate(_records(values), 1)
|
|
168
|
+
]
|
|
169
|
+
if normalized == "osv":
|
|
170
|
+
vulnerabilities = _records(output.get("vulns"))
|
|
171
|
+
for result in _records(output.get("results")):
|
|
172
|
+
vulnerabilities.extend(_records(result.get("vulnerabilities")))
|
|
173
|
+
for package in _records(result.get("packages")):
|
|
174
|
+
vulnerabilities.extend(_records(package.get("vulnerabilities")))
|
|
175
|
+
return [
|
|
176
|
+
{
|
|
177
|
+
"id": item.get("id"),
|
|
178
|
+
"title": item.get("summary") or item.get("details") or "OSV vulnerability",
|
|
179
|
+
"severity": _severity(item.get("severity", "high")),
|
|
180
|
+
"mappings": {"aliases": item.get("aliases", [])},
|
|
181
|
+
}
|
|
182
|
+
for item in vulnerabilities
|
|
183
|
+
]
|
|
184
|
+
if normalized == "presidio":
|
|
185
|
+
return [
|
|
186
|
+
{
|
|
187
|
+
"id": f"{item.get('entity_type', 'PII')}:{item.get('start', index)}",
|
|
188
|
+
"title": f"Detected {item.get('entity_type', 'sensitive data')}",
|
|
189
|
+
"severity": "HIGH",
|
|
190
|
+
"confidence": item.get("score", 0.5),
|
|
191
|
+
}
|
|
192
|
+
for index, item in enumerate(_records(output.get("results")), 1)
|
|
193
|
+
]
|
|
194
|
+
if normalized == "llmguard" and output.get("is_valid") is False:
|
|
195
|
+
return [
|
|
196
|
+
{
|
|
197
|
+
"id": output.get("scanner", "llmguard"),
|
|
198
|
+
"title": output.get("reason", "LLM Guard rejected content"),
|
|
199
|
+
"severity": "HIGH",
|
|
200
|
+
"confidence": output.get("risk_score", 0.8),
|
|
201
|
+
}
|
|
202
|
+
]
|
|
203
|
+
if normalized == "modelscan":
|
|
204
|
+
values = output.get("issues", output.get("results", []))
|
|
205
|
+
return [
|
|
206
|
+
_common_record(item, normalized, index)
|
|
207
|
+
for index, item in enumerate(_records(values), 1)
|
|
208
|
+
]
|
|
209
|
+
for key in ("results", "vulnerabilities", "issues", "failures", "probes", "tests"):
|
|
210
|
+
records = _records(output.get(key))
|
|
211
|
+
if records:
|
|
212
|
+
return [_common_record(item, normalized, index) for index, item in enumerate(records, 1)]
|
|
213
|
+
return []
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def _sarif_records(output: dict[str, object]) -> list[dict[str, Any]]:
|
|
217
|
+
records = []
|
|
218
|
+
for run in _records(output.get("runs")):
|
|
219
|
+
for item in _records(run.get("results")):
|
|
220
|
+
message = item.get("message", {})
|
|
221
|
+
records.append(
|
|
222
|
+
{
|
|
223
|
+
"id": item.get("ruleId"),
|
|
224
|
+
"title": message.get("text", "SARIF finding")
|
|
225
|
+
if isinstance(message, dict)
|
|
226
|
+
else str(message),
|
|
227
|
+
"severity": _severity(item.get("level")),
|
|
228
|
+
}
|
|
229
|
+
)
|
|
230
|
+
return records
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def _records(value: object) -> list[dict[str, Any]]:
|
|
234
|
+
return [item for item in value if isinstance(item, dict)] if isinstance(value, list) else []
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def _common_record(item: dict[str, Any], engine: str, index: int) -> dict[str, Any]:
|
|
238
|
+
identifier = next(
|
|
239
|
+
(item[key] for key in ("id", "test_id", "test", "name", "type") if item.get(key)),
|
|
240
|
+
f"{engine}-{index}",
|
|
241
|
+
)
|
|
242
|
+
title = next(
|
|
243
|
+
(
|
|
244
|
+
item[key]
|
|
245
|
+
for key in ("title", "message", "description", "reason", "name")
|
|
246
|
+
if item.get(key)
|
|
247
|
+
),
|
|
248
|
+
f"{engine} finding",
|
|
249
|
+
)
|
|
250
|
+
status = str(item.get("status", item.get("verdict", ""))).casefold()
|
|
251
|
+
verdict = "PASS" if status in {"pass", "passed", "safe"} else "LIKELY_VIOLATION"
|
|
252
|
+
return {
|
|
253
|
+
"id": str(identifier),
|
|
254
|
+
"title": str(title),
|
|
255
|
+
"severity": _severity(item.get("severity", item.get("level", item.get("risk")))),
|
|
256
|
+
"verdict": verdict,
|
|
257
|
+
"confidence": item.get("confidence", item.get("score", 0.5)),
|
|
258
|
+
"mappings": item.get("mappings", {}),
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
def _severity(value: object) -> str:
|
|
263
|
+
return {
|
|
264
|
+
"critical": "CRITICAL",
|
|
265
|
+
"error": "HIGH",
|
|
266
|
+
"high": "HIGH",
|
|
267
|
+
"warning": "MEDIUM",
|
|
268
|
+
"medium": "MEDIUM",
|
|
269
|
+
"low": "LOW",
|
|
270
|
+
"note": "LOW",
|
|
271
|
+
"info": "INFO",
|
|
272
|
+
}.get(str(value).casefold(), "MEDIUM")
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
def _verdict(value: object) -> Verdict:
|
|
276
|
+
try:
|
|
277
|
+
return Verdict(str(value or "LIKELY_VIOLATION").upper())
|
|
278
|
+
except ValueError:
|
|
279
|
+
return Verdict.INCONCLUSIVE
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
def _confidence(value: object) -> float:
|
|
283
|
+
candidate = value if value is not None else 0.5
|
|
284
|
+
if not isinstance(candidate, (str, int, float)):
|
|
285
|
+
return 0.0
|
|
286
|
+
try:
|
|
287
|
+
return min(1.0, max(0.0, float(candidate)))
|
|
288
|
+
except ValueError:
|
|
289
|
+
return 0.0
|