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.
Files changed (44) hide show
  1. secgraphai/__init__.py +102 -0
  2. secgraphai/adapters.py +289 -0
  3. secgraphai/attacks.py +375 -0
  4. secgraphai/canary.py +41 -0
  5. secgraphai/cli.py +778 -0
  6. secgraphai/config.py +102 -0
  7. secgraphai/core.py +151 -0
  8. secgraphai/dashboard.py +475 -0
  9. secgraphai/discovery.py +465 -0
  10. secgraphai/evaluation.py +117 -0
  11. secgraphai/graph.py +191 -0
  12. secgraphai/instrumentation.py +178 -0
  13. secgraphai/intelligence.py +786 -0
  14. secgraphai/invariants.py +180 -0
  15. secgraphai/lifecycle.py +414 -0
  16. secgraphai/model.py +76 -0
  17. secgraphai/multimodal.py +38 -0
  18. secgraphai/packs/__init__.py +1 -0
  19. secgraphai/packs/official_ed25519.pub +1 -0
  20. secgraphai/packs/prompt_injection_core.yaml +47 -0
  21. secgraphai/plugins.py +201 -0
  22. secgraphai/policy.py +144 -0
  23. secgraphai/provenance.py +73 -0
  24. secgraphai/reporting.py +184 -0
  25. secgraphai/research.py +61 -0
  26. secgraphai/roles.py +130 -0
  27. secgraphai/runtime.py +304 -0
  28. secgraphai/scanner.py +593 -0
  29. secgraphai/schemas.py +140 -0
  30. secgraphai/security.py +211 -0
  31. secgraphai/security_modules.py +897 -0
  32. secgraphai/self_security.py +212 -0
  33. secgraphai/static/app.js +11 -0
  34. secgraphai/static/index.html +43 -0
  35. secgraphai/static/styles.css +9 -0
  36. secgraphai/storage.py +260 -0
  37. secgraphai/supply_chain.py +64 -0
  38. secgraphai/targets.py +368 -0
  39. secgraphai/validators.py +284 -0
  40. secgraphai-1.0.0rc1.dist-info/METADATA +208 -0
  41. secgraphai-1.0.0rc1.dist-info/RECORD +44 -0
  42. secgraphai-1.0.0rc1.dist-info/WHEEL +4 -0
  43. secgraphai-1.0.0rc1.dist-info/entry_points.txt +2 -0
  44. 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