cernus-plugins 0.1.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.
@@ -0,0 +1,158 @@
1
+ """SARIF 2.1.0 — connector (semgrep, CodeQL, bandit, genesis-cli …) and exporter."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from pathlib import Path
7
+ from typing import Any
8
+
9
+ from cernus_core.model import Finding, IngestBatch, Severity, Source, new_id, now_iso
10
+ from cernus_core.plugins import Connector, Exporter, register_connector, register_exporter
11
+
12
+ _LEVEL_TO_SEV = {"error": "high", "warning": "medium", "note": "low", "none": "info"}
13
+ _SEV_TO_LEVEL = {
14
+ "critical": "error",
15
+ "high": "error",
16
+ "medium": "warning",
17
+ "low": "note",
18
+ "info": "none",
19
+ }
20
+
21
+
22
+ @register_connector
23
+ class SarifConnector(Connector):
24
+ name = "sarif"
25
+ description = "SARIF 2.1.0 static-analysis logs"
26
+ tools = ("semgrep", "codeql", "bandit", "genesis-cli")
27
+
28
+ def detect(self, path: Path, document: dict[str, Any] | None = None) -> bool:
29
+ return bool(document) and document.get("version") == "2.1.0" and "runs" in document
30
+
31
+ def ingest(
32
+ self, path: Path, document: dict[str, Any] | None = None, **context: Any
33
+ ) -> IngestBatch:
34
+ doc = document if document is not None else json.loads(path.read_text())
35
+ batch = IngestBatch(source=Source(tool="sarif", connector=self.name))
36
+ seen = now_iso()
37
+ for run in doc.get("runs") or []:
38
+ driver = (run.get("tool") or {}).get("driver") or {}
39
+ source = Source(
40
+ tool=driver.get("name", "sarif").lower(),
41
+ version=driver.get("version"),
42
+ connector=self.name,
43
+ )
44
+ rules = {r.get("id"): r for r in driver.get("rules") or []}
45
+ for res in run.get("results") or []:
46
+ rule_id = res.get("ruleId") or "unknown"
47
+ rule = rules.get(rule_id, {})
48
+ sev = _severity(res, rule)
49
+ loc = _location(res)
50
+ batch.findings.append(
51
+ Finding(
52
+ id=new_id("fnd"),
53
+ title=(rule.get("shortDescription") or {}).get("text") or rule_id,
54
+ severity=sev,
55
+ source=source,
56
+ source_ref=rule_id,
57
+ category="sast",
58
+ detail=(res.get("message") or {}).get("text"),
59
+ location=loc,
60
+ remediation=(rule.get("help") or {}).get("text"),
61
+ asset_id=context.get("asset_id"),
62
+ deployment_id=context.get("deployment_id"),
63
+ system_id=context.get("system_id"),
64
+ first_seen=seen,
65
+ last_seen=seen,
66
+ tags=[
67
+ t
68
+ for t in (rule.get("properties") or {}).get("tags", [])
69
+ if isinstance(t, str)
70
+ ][:10],
71
+ raw=res,
72
+ )
73
+ )
74
+ if batch.findings:
75
+ batch.source = batch.findings[0].source
76
+ return batch
77
+
78
+
79
+ def _severity(res: dict, rule: dict) -> str:
80
+ props = rule.get("properties") or {}
81
+ if "security-severity" in props: # GitHub convention: CVSS-like 0–10 string
82
+ try:
83
+ s = float(props["security-severity"])
84
+ return "critical" if s >= 9 else "high" if s >= 7 else "medium" if s >= 4 else "low"
85
+ except ValueError:
86
+ pass
87
+ level = res.get("level") or (rule.get("defaultConfiguration") or {}).get("level") or "warning"
88
+ return _LEVEL_TO_SEV.get(level, "medium")
89
+
90
+
91
+ def _location(res: dict) -> str | None:
92
+ for loc in res.get("locations") or []:
93
+ phys = loc.get("physicalLocation") or {}
94
+ uri = (phys.get("artifactLocation") or {}).get("uri")
95
+ line = (phys.get("region") or {}).get("startLine")
96
+ if uri:
97
+ return f"{uri}:{line}" if line else uri
98
+ return None
99
+
100
+
101
+ @register_exporter
102
+ class SarifExporter(Exporter):
103
+ name = "sarif"
104
+ description = "Write findings as SARIF 2.1.0 (one run per source tool)"
105
+ formats = ("sarif",)
106
+
107
+ def export(self, fmt: str, batch: IngestBatch, out: Path, **options: Any) -> Path:
108
+ by_tool: dict[str, list[Finding]] = {}
109
+ for f in batch.findings:
110
+ by_tool.setdefault(f.source.tool, []).append(f)
111
+ runs = []
112
+ for tool, findings in by_tool.items():
113
+ rules: dict[str, dict] = {}
114
+ results = []
115
+ for f in findings:
116
+ rid = f.source_ref or f.fingerprint[:12]
117
+ rules.setdefault(rid, {"id": rid, "shortDescription": {"text": f.title}})
118
+ result: dict[str, Any] = {
119
+ "ruleId": rid,
120
+ "level": _SEV_TO_LEVEL[f.severity.value],
121
+ "message": {"text": f.detail or f.title},
122
+ "fingerprints": {"cernus/v1": f.fingerprint},
123
+ "properties": {
124
+ "cernus": {
125
+ "id": f.id,
126
+ "severity": f.severity.value,
127
+ "status": f.status.value,
128
+ }
129
+ },
130
+ }
131
+ if f.location:
132
+ uri, _, line = f.location.partition(":")
133
+ region = {"startLine": int(line)} if line.isdigit() else {}
134
+ result["locations"] = [
135
+ {
136
+ "physicalLocation": {
137
+ "artifactLocation": {"uri": uri},
138
+ **({"region": region} if region else {}),
139
+ }
140
+ }
141
+ ]
142
+ results.append(result)
143
+ runs.append(
144
+ {
145
+ "tool": {"driver": {"name": tool, "rules": list(rules.values())}},
146
+ "results": results,
147
+ }
148
+ )
149
+ doc = {
150
+ "$schema": "https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/sarif-2.1.0.json",
151
+ "version": "2.1.0",
152
+ "runs": runs,
153
+ }
154
+ out.write_text(json.dumps(doc, indent=2))
155
+ return out
156
+
157
+
158
+ __all__ = ["SarifConnector", "SarifExporter", "Severity"]
cernus_plugins/syft.py ADDED
@@ -0,0 +1,50 @@
1
+ """Syft scanner wrapper: produce a CycloneDX SBOM for an image or directory.
2
+
3
+ Normalization of the SBOM itself is the :mod:`cyclonedx` connector's job.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ import subprocess
9
+ from pathlib import Path
10
+
11
+ from cernus_core.plugins import Scanner, ScanResult, register_scanner
12
+
13
+
14
+ @register_scanner
15
+ class SyftScanner(Scanner):
16
+ name = "syft"
17
+ description = "Generate a CycloneDX SBOM with syft"
18
+ binary = "syft"
19
+ targets = ("image", "fs")
20
+
21
+ def version(self) -> str | None:
22
+ try:
23
+ out = subprocess.run(
24
+ [self.binary, "version", "-o", "raw"],
25
+ capture_output=True,
26
+ text=True,
27
+ timeout=20,
28
+ check=False,
29
+ ).stdout
30
+ return out.strip() or None
31
+ except (OSError, subprocess.TimeoutExpired):
32
+ return None
33
+
34
+ def scan(self, target_kind: str, target: str, workdir: Path) -> ScanResult:
35
+ if target_kind not in self.targets:
36
+ raise ValueError(f"syft scanner does not support target kind {target_kind!r}")
37
+ workdir.mkdir(parents=True, exist_ok=True)
38
+ slug = "".join(c if c.isalnum() else "-" for c in target).strip("-")[:80]
39
+ out = workdir / f"syft-{slug}.cdx.json"
40
+ src = target if target_kind == "image" else f"dir:{target}"
41
+ argv = [self.binary, src, "-o", f"cyclonedx-json={out}", "-q"]
42
+ proc = subprocess.run(argv, capture_output=True, text=True, check=False, timeout=1800)
43
+ return ScanResult(
44
+ tool="syft",
45
+ output_path=out if out.exists() else None,
46
+ connector="cyclonedx",
47
+ argv=argv,
48
+ returncode=proc.returncode,
49
+ stderr=proc.stderr[-4000:],
50
+ )
@@ -0,0 +1,294 @@
1
+ """Trivy — connector for `trivy … --format json` output, and a scanner wrapper.
2
+
3
+ Covers image scans (``ArtifactType: container_image``), filesystem scans, and
4
+ SBOM scans (``trivy sbom``). Misconfiguration and secret results are mapped to
5
+ their own categories when present.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import json
11
+ import subprocess
12
+ from pathlib import Path
13
+ from typing import Any
14
+
15
+ from cernus_core.model import (
16
+ Asset,
17
+ Component,
18
+ EvidenceRecord,
19
+ Finding,
20
+ IngestBatch,
21
+ Source,
22
+ Vuln,
23
+ new_id,
24
+ now_iso,
25
+ )
26
+ from cernus_core.plugins import Connector, Scanner, ScanResult, register_connector, register_scanner
27
+
28
+ _KIND_BY_ARTIFACT = {
29
+ "container_image": "container_image",
30
+ "filesystem": "repository",
31
+ "repository": "repository",
32
+ "cyclonedx": "container_image",
33
+ "spdx": "container_image",
34
+ }
35
+
36
+
37
+ @register_connector
38
+ class TrivyConnector(Connector):
39
+ name = "trivy"
40
+ description = (
41
+ "Trivy JSON reports (image, fs, sbom): vulnerabilities, misconfigurations, secrets"
42
+ )
43
+ tools = ("trivy",)
44
+
45
+ def detect(self, path: Path, document: dict[str, Any] | None = None) -> bool:
46
+ return (
47
+ bool(document)
48
+ and "SchemaVersion" in document
49
+ and "Results" in document
50
+ and ("ArtifactName" in document or "ArtifactType" in document)
51
+ )
52
+
53
+ def ingest(
54
+ self, path: Path, document: dict[str, Any] | None = None, **context: Any
55
+ ) -> IngestBatch:
56
+ doc = document if document is not None else json.loads(path.read_text())
57
+ version = (doc.get("Trivy") or {}).get("Version")
58
+ source = Source(tool="trivy", version=version, connector=self.name)
59
+ batch = IngestBatch(source=source)
60
+
61
+ artifact = doc.get("ArtifactName") or path.stem
62
+ artifact_type = doc.get("ArtifactType", "container_image")
63
+ meta = doc.get("Metadata") or {}
64
+ digest = (meta.get("RepoDigests") or [None])[0]
65
+ image_id = meta.get("ImageID")
66
+
67
+ asset_id = context.get("asset_id")
68
+ if asset_id is None:
69
+ # Identity facet is the image *reference* (name:tag) so that trivy,
70
+ # syft, and the Genesis deployment record reconcile to one asset;
71
+ # the digest rides along as an identifier for exact matching.
72
+ asset_id = Asset.id_for(
73
+ _KIND_BY_ARTIFACT.get(artifact_type, "other"), normalize_ref(artifact)
74
+ )
75
+ identifiers = {"image_ref": artifact}
76
+ if digest:
77
+ identifiers["digest"] = digest.split("@")[-1]
78
+ elif image_id:
79
+ identifiers["digest"] = image_id
80
+ batch.assets.append(
81
+ Asset(
82
+ id=asset_id,
83
+ asset_kind=_KIND_BY_ARTIFACT.get(artifact_type, "other"),
84
+ name=artifact,
85
+ identifiers=identifiers,
86
+ system_id=context.get("system_id"),
87
+ deployment_id=context.get("deployment_id"),
88
+ environment=context.get("environment", "unknown"),
89
+ source=source,
90
+ )
91
+ )
92
+
93
+ seen = now_iso()
94
+ context = {**context, "scan_scope": artifact_type}
95
+ for result in doc.get("Results") or []:
96
+ target = result.get("Target")
97
+ for v in result.get("Vulnerabilities") or []:
98
+ batch.findings.append(self._vuln(v, target, asset_id, source, seen, context))
99
+ for m in result.get("Misconfigurations") or []:
100
+ batch.findings.append(self._misconf(m, target, asset_id, source, seen, context))
101
+ for s in result.get("Secrets") or []:
102
+ batch.findings.append(self._secret(s, target, asset_id, source, seen, context))
103
+
104
+ batch.evidence.append(
105
+ EvidenceRecord(
106
+ id=new_id("ev"),
107
+ record_kind="scan",
108
+ subject_kind="asset",
109
+ subject_id=asset_id,
110
+ produced_by=f"trivy {version or ''}".strip(),
111
+ summary=f"trivy scan of {artifact}: {len(batch.findings)} findings",
112
+ payload={
113
+ "artifact": artifact,
114
+ "artifact_type": artifact_type,
115
+ "created_at": doc.get("CreatedAt"),
116
+ "report_id": doc.get("ReportID"),
117
+ "os": meta.get("OS"),
118
+ "counts": _counts(batch.findings),
119
+ },
120
+ )
121
+ )
122
+ return batch
123
+
124
+ @staticmethod
125
+ def _vuln(
126
+ v: dict, target: str | None, asset_id: str, source: Source, seen: str, ctx: dict
127
+ ) -> Finding:
128
+ cvss_block = v.get("CVSS") or {}
129
+ score, vector = None, None
130
+ # Prefer NVD v3, then any v3, then v2.
131
+ for key in ("nvd", *sorted(k for k in cvss_block if k != "nvd")):
132
+ c = cvss_block.get(key) or {}
133
+ if c.get("V3Score") is not None:
134
+ score, vector = c["V3Score"], c.get("V3Vector")
135
+ break
136
+ if score is None:
137
+ for c in cvss_block.values():
138
+ if c.get("V2Score") is not None:
139
+ score, vector = c["V2Score"], c.get("V2Vector")
140
+ break
141
+ purl = (v.get("PkgIdentifier") or {}).get("PURL")
142
+ return Finding(
143
+ id=new_id("fnd"),
144
+ title=v.get("Title") or f"{v.get('VulnerabilityID')} in {v.get('PkgName')}",
145
+ severity=v.get("Severity"),
146
+ source=source,
147
+ source_ref=v.get("VulnerabilityID"),
148
+ category="vulnerability",
149
+ detail=v.get("Description"),
150
+ location=target,
151
+ remediation=(
152
+ f"Upgrade {v.get('PkgName')} to {v['FixedVersion']}"
153
+ if v.get("FixedVersion")
154
+ else None
155
+ ),
156
+ vuln=Vuln(
157
+ id=v.get("VulnerabilityID", ""),
158
+ cwe=list(v.get("CweIDs") or []),
159
+ cvss=score,
160
+ cvss_vector=vector,
161
+ published=v.get("PublishedDate"),
162
+ references=list(v.get("References") or [])[:5],
163
+ ),
164
+ component=Component(
165
+ name=v.get("PkgName", ""),
166
+ purl=purl,
167
+ version=v.get("InstalledVersion"),
168
+ fixed_version=v.get("FixedVersion"),
169
+ type=(purl.split(":")[1].split("/")[0] if purl else None),
170
+ ),
171
+ asset_id=asset_id,
172
+ deployment_id=ctx.get("deployment_id"),
173
+ system_id=ctx.get("system_id"),
174
+ scan_scope=ctx.get("scan_scope"),
175
+ first_seen=seen,
176
+ last_seen=seen,
177
+ tags=[t for t in (v.get("Status"),) if t],
178
+ raw=v,
179
+ )
180
+
181
+ @staticmethod
182
+ def _misconf(
183
+ m: dict, target: str | None, asset_id: str, source: Source, seen: str, ctx: dict
184
+ ) -> Finding:
185
+ return Finding(
186
+ id=new_id("fnd"),
187
+ title=m.get("Title") or m.get("ID", "misconfiguration"),
188
+ severity=m.get("Severity"),
189
+ source=source,
190
+ source_ref=m.get("ID") or m.get("AVDID"),
191
+ category="misconfiguration",
192
+ detail=m.get("Description") or m.get("Message"),
193
+ location=target,
194
+ remediation=m.get("Resolution"),
195
+ asset_id=asset_id,
196
+ deployment_id=ctx.get("deployment_id"),
197
+ system_id=ctx.get("system_id"),
198
+ scan_scope=ctx.get("scan_scope"),
199
+ first_seen=seen,
200
+ last_seen=seen,
201
+ raw=m,
202
+ )
203
+
204
+ @staticmethod
205
+ def _secret(
206
+ s: dict, target: str | None, asset_id: str, source: Source, seen: str, ctx: dict
207
+ ) -> Finding:
208
+ return Finding(
209
+ id=new_id("fnd"),
210
+ title=s.get("Title") or f"Secret: {s.get('RuleID')}",
211
+ severity=s.get("Severity"),
212
+ source=source,
213
+ source_ref=s.get("RuleID"),
214
+ category="secret",
215
+ detail=s.get("Match"),
216
+ location=f"{target}:{s.get('StartLine')}" if s.get("StartLine") else target,
217
+ remediation="Remove the secret from the artifact and rotate it.",
218
+ asset_id=asset_id,
219
+ deployment_id=ctx.get("deployment_id"),
220
+ system_id=ctx.get("system_id"),
221
+ scan_scope=ctx.get("scan_scope"),
222
+ first_seen=seen,
223
+ last_seen=seen,
224
+ raw={k: v for k, v in s.items() if k != "Match"}, # never persist the matched secret
225
+ )
226
+
227
+
228
+ def _counts(findings: list[Finding]) -> dict[str, int]:
229
+ out: dict[str, int] = {}
230
+ for f in findings:
231
+ out[f.severity.value] = out.get(f.severity.value, 0) + 1
232
+ return out
233
+
234
+
235
+ @register_scanner
236
+ class TrivyScanner(Scanner):
237
+ name = "trivy"
238
+ description = "Run trivy against an image, directory, or SBOM"
239
+ binary = "trivy"
240
+ targets = ("image", "fs", "sbom")
241
+
242
+ def version(self) -> str | None:
243
+ try:
244
+ out = subprocess.run(
245
+ [self.binary, "--version"], capture_output=True, text=True, timeout=20, check=False
246
+ ).stdout
247
+ return out.strip().splitlines()[0].replace("Version:", "").strip() or None
248
+ except (OSError, subprocess.TimeoutExpired):
249
+ return None
250
+
251
+ def scan(self, target_kind: str, target: str, workdir: Path) -> ScanResult:
252
+ if target_kind not in self.targets:
253
+ raise ValueError(f"trivy scanner does not support target kind {target_kind!r}")
254
+ workdir.mkdir(parents=True, exist_ok=True)
255
+ out = workdir / f"trivy-{target_kind}-{_slug(target)}.json"
256
+ argv = [
257
+ self.binary,
258
+ target_kind,
259
+ "--skip-version-check",
260
+ "--format",
261
+ "json",
262
+ "--output",
263
+ str(out),
264
+ target,
265
+ ]
266
+ if target_kind == "image":
267
+ argv[2:2] = ["--scanners", "vuln,secret"]
268
+ proc = subprocess.run(argv, capture_output=True, text=True, check=False, timeout=1800)
269
+ return ScanResult(
270
+ tool="trivy",
271
+ output_path=out if out.exists() else None,
272
+ connector="trivy",
273
+ argv=argv,
274
+ returncode=proc.returncode,
275
+ stderr=proc.stderr[-4000:],
276
+ )
277
+
278
+
279
+ def normalize_ref(ref: str) -> str:
280
+ """``docker.io/library/python:3.11-slim`` and ``python:3.11-slim`` are one asset."""
281
+ ref = ref.split("@")[0]
282
+ for prefix in (
283
+ "docker.io/library/",
284
+ "docker.io/",
285
+ "index.docker.io/library/",
286
+ "index.docker.io/",
287
+ ):
288
+ if ref.startswith(prefix):
289
+ ref = ref[len(prefix) :]
290
+ return ref if ":" in ref.rsplit("/", 1)[-1] else f"{ref}:latest"
291
+
292
+
293
+ def _slug(s: str) -> str:
294
+ return "".join(c if c.isalnum() else "-" for c in s).strip("-")[:80]
@@ -0,0 +1,35 @@
1
+ Metadata-Version: 2.5
2
+ Name: cernus-plugins
3
+ Version: 0.1.0
4
+ Summary: First-party Cernus connectors, scanners, and exporters: Genesis, Trivy, Syft/CycloneDX, SARIF, OSCAL.
5
+ Project-URL: Homepage, https://github.com/Inceptium-ai/cernus-framework
6
+ Project-URL: Repository, https://github.com/Inceptium-ai/cernus-framework
7
+ Author-email: Inceptium <opensource@inceptium.ai>
8
+ License-Expression: Apache-2.0
9
+ License-File: LICENSE
10
+ Classifier: Development Status :: 3 - Alpha
11
+ Classifier: License :: OSI Approved :: Apache Software License
12
+ Classifier: Programming Language :: Python :: 3.12
13
+ Classifier: Programming Language :: Python :: 3.13
14
+ Classifier: Topic :: Security
15
+ Requires-Python: >=3.12
16
+ Requires-Dist: cernus-core>=0.1.0
17
+ Requires-Dist: pyyaml>=6.0
18
+ Description-Content-Type: text/markdown
19
+
20
+ # cernus-plugins
21
+
22
+ First-party plugins for [Cernus](https://github.com/Inceptium-ai/cernus-framework).
23
+ Each one wraps a tool you already run; none re-implements a scanner.
24
+
25
+ | Plugin | Kind | What it does |
26
+ |---|---|---|
27
+ | `genesis` | connector | Genesis `EvidencePackage` + `DeliveryManifest` → System, Deployment, Findings |
28
+ | `trivy` | connector + scanner | `trivy image/sbom --format json` → Findings with CVE/CVSS/purl; runs trivy |
29
+ | `syft` | scanner | `syft <image> -o cyclonedx-json` → SBOM |
30
+ | `cyclonedx` | connector | CycloneDX SBOM → Sbom + component Assets |
31
+ | `sarif` | connector + exporter | SARIF 2.1.0 in (semgrep, CodeQL, …) and out |
32
+ | `oscal` | exporter | Findings → OSCAL 1.1 POA&M (minimal, schema-shaped) |
33
+
34
+ Plugins register through entry points (`cernus.connectors`, `cernus.scanners`,
35
+ `cernus.exporters`). Write your own the same way — see `cernus_core.plugins`.
@@ -0,0 +1,12 @@
1
+ cernus_plugins/__init__.py,sha256=2QAXxvntSldkQujJ4fSWYPyvcOTnaz56Tq968CSfcG8,175
2
+ cernus_plugins/cyclonedx.py,sha256=pj3lOW_wp6CD6qOLl-66s7EJHnAiymUpm5hvJdWwxLU,4306
3
+ cernus_plugins/genesis.py,sha256=zsuMkPd_gAIMUiR-pAmCBxbapIWDlKWVOu4doVds5v4,12444
4
+ cernus_plugins/oscal.py,sha256=riAe7wYectYHqHwST2v_OaJQEGwF5ppzHW94PKMpeRk,10035
5
+ cernus_plugins/sarif.py,sha256=y91Cw8aTGyj8PiPehfMMSXatlgHnAE_ZPlFu9Eqw_wg,6250
6
+ cernus_plugins/syft.py,sha256=mQsyEAdbFTBICDEInaSc7BdfT8-05-7ST_fIOo3koxA,1763
7
+ cernus_plugins/trivy.py,sha256=d4V_VotAHmyH8XDa_azRK9U75hCKbycgsmiChRPQGfU,10833
8
+ cernus_plugins-0.1.0.dist-info/METADATA,sha256=MXvyEtZ1L1rvVSuFQTom_4scDAFkEPPmDQG3sCb53pc,1683
9
+ cernus_plugins-0.1.0.dist-info/WHEEL,sha256=zOwg4jB6zX2kU910N-cMawjivD6tO8NEWvE12je1bVk,87
10
+ cernus_plugins-0.1.0.dist-info/entry_points.txt,sha256=i9x-BMBZc_BUHQU9jBiQSpoqI5OojZhXhfPSi5cOnZg,420
11
+ cernus_plugins-0.1.0.dist-info/licenses/LICENSE,sha256=z8d0m5b2O9McPEK1xHG_dWgUBT6EfBDz6wA0F7xSPTA,11358
12
+ cernus_plugins-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.32.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,13 @@
1
+ [cernus.connectors]
2
+ cyclonedx = cernus_plugins.cyclonedx:CycloneDXConnector
3
+ genesis = cernus_plugins.genesis:GenesisConnector
4
+ sarif = cernus_plugins.sarif:SarifConnector
5
+ trivy = cernus_plugins.trivy:TrivyConnector
6
+
7
+ [cernus.exporters]
8
+ oscal = cernus_plugins.oscal:OscalExporter
9
+ sarif = cernus_plugins.sarif:SarifExporter
10
+
11
+ [cernus.scanners]
12
+ syft = cernus_plugins.syft:SyftScanner
13
+ trivy = cernus_plugins.trivy:TrivyScanner