@hoanghainh/komagu 1.7.0 → 2.0.0
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.
- package/CHANGELOG.md +39 -0
- package/assess/lsa/CHANGELOG.md +6 -0
- package/assess/lsa/VERSION +1 -1
- package/assess/lsa/adapters/build_adapters.py +16 -8
- package/assess/lsa/adapters/codex/README.md +1 -1
- package/assess/lsa/adapters/generic/README.md +1 -1
- package/assess/lsa/knowledge/severity-and-cwe.md +1 -1
- package/assess/lsa/scripts/baseline.py +5 -2
- package/assess/lsa/scripts/findings_to_sarif.py +24 -1
- package/assess/lsa/tests/test_adapters.py +21 -2
- package/assess/lsa/tests/test_baseline.py +8 -0
- package/assess/lsa/tests/test_contract_groundwork.py +7 -7
- package/operate/cli/__init__.py +1 -1
- package/operate/cli/commands/scan.py +2 -7
- package/operate/cli/commands/test.py +1 -1
- package/operate/cli/env.py +4 -20
- package/operate/cross_user_tester/persona_matrix.py +5 -1
- package/operate/devsecops/pipeline.py +5 -5
- package/operate/exploit/generator.py +5 -1
- package/operate/lsa_sarif/lsa_to_sarif.py +24 -2
- package/operate/lsa_sarif/sarif_findings.py +96 -0
- package/operate/remediator/runner.py +1 -5
- package/operate/remediator/strategies.py +1 -1
- package/operate/tests/test_cli_baseline.py +1 -9
- package/operate/tests/test_cli_env.py +8 -29
- package/operate/tests/test_cross_user_pipeline.py +2 -2
- package/operate/tests/test_exploit_and_virtual_patch.py +3 -3
- package/operate/tests/test_npm_package.py +2 -16
- package/operate/tests/test_sarif_from_scan.py +156 -0
- package/package.json +2 -4
- package/remediate/authz/README.md +1 -1
- package/remediate/authz/dotnet/src/Komagu.Security.Authorization/DefaultKomaguAuthorizationService.cs +1 -1
- package/remediate/authz/dotnet/src/Komagu.Security.Authorization/SecurityAuditEvent.cs +2 -2
- package/remediate/authz/golang/authz.go +1 -1
- package/remediate/authz/nodejs/src/service.js +1 -1
- package/remediate/authz/nodejs/tests/test-authz.js +1 -1
- package/remediate/authz/patcher.py +3 -4
- package/remediate/authz/python/komagu_authz/models.py +2 -2
- package/remediate/authz/python/tests/test_fastapi_adapter.py +1 -1
- package/remediate/authz/python/tests/test_fastapi_d5_crosstest.py +1 -1
- package/remediate/authz/python/tests/test_python_authz.py +3 -3
- package/remediate/authz/spring/src/main/kotlin/io/komagu/security/authorization/DefaultKomaguSecurityManager.kt +1 -1
- package/remediate/authz/spring/src/main/kotlin/io/komagu/security/authorization/SecurityAuditEvent.kt +1 -2
- package/remediate/authz/tests/test_bola_patcher.py +1 -1
- package/remediate/edge/coraza/rules/coraza-crs-custom.conf +3 -4
- package/remediate/edge/coraza/virtual_patcher.py +4 -6
- package/remediate/edge/docker-compose.yml +1 -1
- package/remediate/ekyc/server/jpki_verifier.py +2 -8
- package/remediate/ekyc/server/tokenization.py +2 -8
- package/remediate/identity/README.md +5 -5
- package/remediate/identity/dotnet/src/Komagu.Security.Identity/KomaguOidcOptions.cs +2 -2
- package/remediate/identity/dotnet/tests/Komagu.Security.Identity.Tests/TestTokenGenerator.cs +1 -1
- package/remediate/identity/dotnet/tests/Komagu.Security.Identity.Tests/TokenValidationTests.cs +1 -1
- package/remediate/identity/python/komagu_identity/oidc_validator.py +2 -8
- package/remediate/identity/python/tests/test_oidc_validator.py +1 -1
- package/remediate/identity/spring/src/main/kotlin/io/komagu/security/identity/KomaguOidcProperties.kt +2 -2
- package/remediate/identity/spring/src/test/kotlin/io/komagu/security/identity/KomaguIdentityAutoConfigurationTest.kt +2 -2
- package/remediate/identity/spring/src/test/kotlin/io/komagu/security/identity/TestJwtFactory.kt +1 -1
- package/remediate/identity/spring/src/test/kotlin/io/komagu/security/identity/TokenValidationIntegrationTest.kt +1 -1
- package/remediate/identity/test-env/realm-komagu.json +1 -1
- package/remediate/legacy_shims/coldfusion/KomaguSecurityShim.cfc +5 -5
- package/remediate/query/README.md +1 -1
- package/remediate/query/dotnet/src/Komagu.Security.Query.Analyzers/DiagnosticIds.cs +1 -1
- package/remediate/query/dotnet/src/Komagu.Security.Query.Analyzers/Komagu.Security.Query.Analyzers.csproj +1 -1
- package/remediate/query/spring/src/main/kotlin/io/komagu/security/query/detekt/KomaguRuleSetProvider.kt +1 -1
- package/remediate/resilience/dr/dr_manager.py +2 -8
- package/remediate/resilience/dr/runbook.md +2 -2
- package/remediate/resilience/terraform/variables.tf +1 -1
- package/remediate/tenancy/audit/audit_trail.py +2 -8
- package/remediate/tenancy/crypto/byok_manager.py +2 -8
- package/remediate/tenancy/middleware/tenant_context.py +2 -8
- package/remediate/tenancy/sql/rls_isolation.sql +11 -11
- package/remediate/tenancy/sql/schema_per_tenant.sql +7 -7
- package/remediate/tenancy/test_sql_postgres.py +1 -1
- package/remediate/tenancy/tests/test_tenancy.py +4 -4
- package/bin/komainu +0 -16
- package/bin/komainu-legacy.js +0 -16
- package/remediate/ekyc/tests/test_legacy_env.py +0 -33
- package/remediate/identity/python/tests/test_legacy_env.py +0 -33
- package/remediate/identity/spring/src/main/kotlin/io/komagu/security/identity/LegacyKomainuPropertiesPostProcessor.kt +0 -60
- package/remediate/identity/spring/src/main/resources/META-INF/spring.factories +0 -2
- package/remediate/identity/spring/src/test/kotlin/io/komagu/security/identity/LegacyKomainuPropertiesPostProcessorTest.kt +0 -86
- package/remediate/resilience/tests/test_legacy_env.py +0 -33
- package/remediate/tenancy/tests/test_legacy_env.py +0 -49
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,45 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to `@hoanghainh/komagu` (published as `komainu-guard` up to v1.7.0) are documented here. From v1.2.2 on, entries are generated by [release-please](https://github.com/googleapis/release-please) from [Conventional Commits](https://www.conventionalcommits.org/). Earlier entries are copied from the [GitHub Releases](https://github.com/hoanghainh1188/komainu/releases).
|
|
4
4
|
|
|
5
|
+
## [2.0.0](https://github.com/hoanghainh1188/komagu/compare/v1.8.0...v2.0.0) (2026-10-30)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
### ⚠ BREAKING CHANGES
|
|
9
|
+
|
|
10
|
+
* rename identifiers kept as "komainu" and remove 1.x shims ([#315](https://github.com/hoanghainh1188/komagu/issues/315))
|
|
11
|
+
Rule IDs, the SARIF fingerprint key, audit event and logger names, WAF tags, the default token audience, the PostgreSQL role and Terraform resource names change; `KOMAINU_*` env vars, `komainu.security.*` properties and the `komainu` npm bins are removed. Read [Migrating to Komagu 2.0](https://github.com/hoanghainh1188/komagu/blob/main/docs/migration-2.0.md) before upgrading.
|
|
12
|
+
|
|
13
|
+
### Features
|
|
14
|
+
|
|
15
|
+
* rename identifiers kept as "komainu" and remove 1.x shims ([#315](https://github.com/hoanghainh1188/komagu/issues/315)) ([cbc63de](https://github.com/hoanghainh1188/komagu/commit/cbc63de3c4e1f90985420b74ba5211740ef4fe25))
|
|
16
|
+
* **test:** read BFLA and mass assignment findings from SARIF in --from-scan and --export-poc ([#313](https://github.com/hoanghainh1188/komagu/issues/313)) ([f91c2fa](https://github.com/hoanghainh1188/komagu/commit/f91c2fa345972bb8da91c503537cafaec7e4ed7b))
|
|
17
|
+
|
|
18
|
+
## [1.8.0](https://github.com/hoanghainh1188/komagu/compare/v1.7.0...v1.8.0) (2026-10-30)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
### Features
|
|
22
|
+
|
|
23
|
+
* **cli:** rename CLI and npm package to komagu (@hoanghainh/komagu) ([#296](https://github.com/hoanghainh1188/komagu/issues/296)) ([a37e8ec](https://github.com/hoanghainh1188/komagu/commit/a37e8ecae013b851dc8d69ba6c9ad7ddc600ae78))
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
### Bug Fixes
|
|
27
|
+
|
|
28
|
+
* **cli:** show "Komagu" in the ASCII banner; point npm links to @hoanghainh/komagu ([#309](https://github.com/hoanghainh1188/komagu/issues/309)) ([533d9b0](https://github.com/hoanghainh1188/komagu/commit/533d9b0de0511825a0704c1f9b2eb5f6546632f7))
|
|
29
|
+
* **spring:** make the member-portal Spring example compile and start ([#305](https://github.com/hoanghainh1188/komagu/issues/305)) ([578197d](https://github.com/hoanghainh1188/komagu/commit/578197d891b2905e03996cd3dbfa9c86445966e0))
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
### Code Refactoring
|
|
33
|
+
|
|
34
|
+
* **dotnet:** rename .NET modules to Komagu.Security.* ([#298](https://github.com/hoanghainh1188/komagu/issues/298)) ([e8ca9d8](https://github.com/hoanghainh1188/komagu/commit/e8ca9d8feffdd6393f37aea8fa1234fe46e73505))
|
|
35
|
+
* **remediate:** rename Python, Go, Node.js, shims and edge modules to Komagu ([#300](https://github.com/hoanghainh1188/komagu/issues/300)) ([54e3daf](https://github.com/hoanghainh1188/komagu/commit/54e3daf8fec80e40626eabb39d9679e834729615))
|
|
36
|
+
* **spring:** rename Spring/Kotlin modules to io.komagu ([#299](https://github.com/hoanghainh1188/komagu/issues/299)) ([7202f6e](https://github.com/hoanghainh1188/komagu/commit/7202f6eef289124761b5f87420c27383be347c92))
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
### Documentation
|
|
40
|
+
|
|
41
|
+
* **changelog:** name the package @hoanghainh/komagu in the header ([#307](https://github.com/hoanghainh1188/komagu/issues/307)) ([c773ba9](https://github.com/hoanghainh1188/komagu/commit/c773ba9b8e871b8b7da04c6c2d4df2c6d2eb31cc))
|
|
42
|
+
* rename LSA, contracts, docs and demo app to Komagu; add ADR-0007 ([#303](https://github.com/hoanghainh1188/komagu/issues/303)) ([9d193d9](https://github.com/hoanghainh1188/komagu/commit/9d193d94f607afb9917fedbe9d4593ca9f8f57c0))
|
|
43
|
+
|
|
5
44
|
## [1.7.0](https://github.com/hoanghainh1188/komainu/compare/v1.6.0...v1.7.0) (2026-10-30)
|
|
6
45
|
|
|
7
46
|
|
package/assess/lsa/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.3.1 — 2026-10-30
|
|
4
|
+
|
|
5
|
+
SARIF replay properties (#289).
|
|
6
|
+
|
|
7
|
+
- **`findings_to_sarif.py`:** each SARIF result's `properties` now carries `finding_id`, `category`, `endpoint`, `attack_vector` (`method`, `path`, `fields`, `readback`, `target_param`) and `consensus_score`, so `komagu test --from-scan` and `--export-poc` can replay BFLA and mass assignment findings from SARIF. `suggested_payload` is not exported, to keep attack strings out of code scanning.
|
|
8
|
+
|
|
3
9
|
## 0.2.3 — 2026-10-26
|
|
4
10
|
|
|
5
11
|
Runtime adapters (#244).
|
package/assess/lsa/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
0.3.
|
|
1
|
+
0.3.1
|
|
@@ -118,8 +118,11 @@ Scripts: `python3 {ref}/scripts/<script>.py` (Python 3.8+, standard library only
|
|
|
118
118
|
print("Next step: In your Claude Code workspace, prompt: 'Run the legacy-security-audit skill. TARGET=src, OUT=lsa-out'")
|
|
119
119
|
|
|
120
120
|
|
|
121
|
-
AGENTS_MD_BEGIN = "<!--
|
|
122
|
-
AGENTS_MD_END = "<!--
|
|
121
|
+
AGENTS_MD_BEGIN = "<!-- komagu-lsa:begin (generated by adapters/build_adapters.py; edits inside this block are overwritten) -->"
|
|
122
|
+
AGENTS_MD_END = "<!-- komagu-lsa:end -->"
|
|
123
|
+
# Pre-2.0 markers, still replaced in place for one major release (#312)
|
|
124
|
+
LEGACY_AGENTS_MD_BEGIN = "<!-- komainu-lsa:begin (generated by adapters/build_adapters.py; edits inside this block are overwritten) -->"
|
|
125
|
+
LEGACY_AGENTS_MD_END = "<!-- komainu-lsa:end -->"
|
|
123
126
|
|
|
124
127
|
|
|
125
128
|
def toml_multiline(text: str) -> str:
|
|
@@ -156,13 +159,16 @@ def agents_md_with_block(path: Path, block: str) -> str:
|
|
|
156
159
|
raise ValueError(f"{path} is not UTF-8 ({e}); convert it, then re-run") from e
|
|
157
160
|
nl = "\r\n" if "\r\n" in current else "\n"
|
|
158
161
|
section = f"{AGENTS_MD_BEGIN}\n{block.strip()}\n{AGENTS_MD_END}\n".replace("\n", nl)
|
|
159
|
-
|
|
162
|
+
begin_marker, end_marker = AGENTS_MD_BEGIN, AGENTS_MD_END
|
|
163
|
+
if begin_marker not in current and end_marker not in current:
|
|
164
|
+
begin_marker, end_marker = LEGACY_AGENTS_MD_BEGIN, LEGACY_AGENTS_MD_END
|
|
165
|
+
begins, ends = current.count(begin_marker), current.count(end_marker)
|
|
160
166
|
if begins == 0 and ends == 0:
|
|
161
167
|
return section if not current.strip() else current.rstrip("\r\n") + nl + nl + section
|
|
162
|
-
start, stop = current.find(
|
|
168
|
+
start, stop = current.find(begin_marker), current.find(end_marker)
|
|
163
169
|
if begins != 1 or ends != 1 or stop < start:
|
|
164
|
-
raise ValueError(f"{path} has unbalanced
|
|
165
|
-
stop += len(
|
|
170
|
+
raise ValueError(f"{path} has unbalanced komagu-lsa markers; remove them by hand, then re-run")
|
|
171
|
+
stop += len(end_marker)
|
|
166
172
|
if current.startswith(nl, stop):
|
|
167
173
|
stop += len(nl)
|
|
168
174
|
return current[:start] + section + current[stop:]
|
|
@@ -252,7 +258,9 @@ def build_cursor(dest: Path) -> None:
|
|
|
252
258
|
print("Next step: In Cursor, open a new chat and invoke: '@lsa-orchestrator run security audit on TARGET=src, OUT=lsa-out'")
|
|
253
259
|
|
|
254
260
|
|
|
255
|
-
GENERATOR = "
|
|
261
|
+
GENERATOR = "komagu-lsa"
|
|
262
|
+
# contract.json files written before 2.0 carry the old value; still recognised for one major release (#312)
|
|
263
|
+
KNOWN_GENERATORS = (GENERATOR, "komainu-lsa")
|
|
256
264
|
|
|
257
265
|
|
|
258
266
|
def check_generic_dest(dest: Path) -> None:
|
|
@@ -266,7 +274,7 @@ def check_generic_dest(dest: Path) -> None:
|
|
|
266
274
|
data = json.loads(contract.read_text(encoding="utf-8"))
|
|
267
275
|
except (UnicodeDecodeError, ValueError):
|
|
268
276
|
data = None
|
|
269
|
-
if not isinstance(data, dict) or data.get("generator")
|
|
277
|
+
if not isinstance(data, dict) or data.get("generator") not in KNOWN_GENERATORS:
|
|
270
278
|
raise ValueError(f"{contract} was not generated by Komagu LSA (no \"generator\": \"{GENERATOR}\"); "
|
|
271
279
|
"choose an empty DEST such as ./lsa-bundle, or move that file away")
|
|
272
280
|
return
|
|
@@ -15,7 +15,7 @@ This creates:
|
|
|
15
15
|
directory up to the repository root.
|
|
16
16
|
- `.codex/agents/lsa-*.toml` — one project custom agent per role
|
|
17
17
|
(`name`, `description`, `developer_instructions`).
|
|
18
|
-
- `AGENTS.md` — an LSA section between `
|
|
18
|
+
- `AGENTS.md` — an LSA section between `komagu-lsa:begin` / `komagu-lsa:end`
|
|
19
19
|
markers. Anything you write outside the markers is kept; re-running the
|
|
20
20
|
generator only replaces the section.
|
|
21
21
|
|
|
@@ -8,7 +8,7 @@ komagu init --ai generic ./lsa-bundle
|
|
|
8
8
|
```
|
|
9
9
|
|
|
10
10
|
It writes `system-prompts/`, `kit/` and `contract.json` into that folder.
|
|
11
|
-
The contract carries `"generator": "
|
|
11
|
+
The contract carries `"generator": "komagu-lsa"`. Re-running refreshes a
|
|
12
12
|
bundle that has this marker. The generator refuses, before writing anything,
|
|
13
13
|
when the folder has a `contract.json` without the marker, or a `kit/` or
|
|
14
14
|
`system-prompts/` folder but no `contract.json`. This keeps a project's own
|
|
@@ -43,7 +43,7 @@ Finding id prefixes: `BOLA-`, `INJ-`, `BFLA-`, `MASS-`.
|
|
|
43
43
|
|
|
44
44
|
`fingerprint` is optional. When present it has the form `kfp1:<64 lowercase hex>`
|
|
45
45
|
and must not depend on line numbers, so the same issue keeps the same value
|
|
46
|
-
across scans. Exporters copy it to SARIF `partialFingerprints["
|
|
46
|
+
across scans. Exporters copy it to SARIF `partialFingerprints["komagu/v1"]`.
|
|
47
47
|
|
|
48
48
|
## SARIF level mapping
|
|
49
49
|
|
|
@@ -35,7 +35,9 @@ from trend import append_history, mttr_by_severity, snapshot # noqa: E402
|
|
|
35
35
|
|
|
36
36
|
SEVERITY_RANK = {"info": 0, "low": 1, "medium": 2, "high": 3, "critical": 4}
|
|
37
37
|
FAIL_ON = ("none", "new", "any")
|
|
38
|
-
SARIF_FINGERPRINT_KEY = "
|
|
38
|
+
SARIF_FINGERPRINT_KEY = "komagu/v1"
|
|
39
|
+
# SARIF baselines written before 2.0 use the old key; still read for one major release (#312)
|
|
40
|
+
LEGACY_SARIF_FINGERPRINT_KEY = "komainu/v1"
|
|
39
41
|
|
|
40
42
|
|
|
41
43
|
class BaselineError(Exception):
|
|
@@ -57,7 +59,8 @@ def _entries_from_sarif(doc: Dict) -> Dict[str, Dict]:
|
|
|
57
59
|
entries: Dict[str, Dict] = {}
|
|
58
60
|
for run in doc.get("runs") or []:
|
|
59
61
|
for result in run.get("results") or []:
|
|
60
|
-
|
|
62
|
+
fingerprints = result.get("partialFingerprints") or {}
|
|
63
|
+
fp = fingerprints.get(SARIF_FINGERPRINT_KEY) or fingerprints.get(LEGACY_SARIF_FINGERPRINT_KEY)
|
|
61
64
|
if not fp:
|
|
62
65
|
continue
|
|
63
66
|
props = result.get("properties") or {}
|
|
@@ -132,6 +132,28 @@ OWASP_TAGS = {
|
|
|
132
132
|
}
|
|
133
133
|
|
|
134
134
|
|
|
135
|
+
# Attack-vector keys that `komagu test --from-scan` and `--export-poc` need to replay a
|
|
136
|
+
# finding from SARIF. suggested_payload is left out so attack strings stay out of code scanning.
|
|
137
|
+
REPLAY_KEYS = ("method", "path", "fields", "readback", "target_param")
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def replay_properties(f: Dict[str, Any]) -> Dict[str, Any]:
|
|
141
|
+
"""Result properties that let a SARIF result be replayed like a findings.json entry."""
|
|
142
|
+
props: Dict[str, Any] = {"finding_id": f.get("finding_id"), "category": f.get("category", "bola")}
|
|
143
|
+
ep = f.get("endpoint")
|
|
144
|
+
if isinstance(ep, dict) and ep.get("path"):
|
|
145
|
+
props["endpoint"] = {k: ep[k] for k in ("method", "path") if ep.get(k)}
|
|
146
|
+
av = f.get("attack_vector")
|
|
147
|
+
if isinstance(av, dict):
|
|
148
|
+
kept = {k: av[k] for k in REPLAY_KEYS if av.get(k) not in (None, "", {})}
|
|
149
|
+
if kept:
|
|
150
|
+
props["attack_vector"] = kept
|
|
151
|
+
cons = f.get("consensus")
|
|
152
|
+
if isinstance(cons, dict) and isinstance(cons.get("score"), (int, float)):
|
|
153
|
+
props["consensus_score"] = cons["score"]
|
|
154
|
+
return props
|
|
155
|
+
|
|
156
|
+
|
|
135
157
|
def normalize_cwe_id(cwe_val: Any) -> str:
|
|
136
158
|
"""Normalize numeric or prefixed CWE ID to standard 'CWE-XXX' format."""
|
|
137
159
|
s = str(cwe_val).strip()
|
|
@@ -319,7 +341,7 @@ def to_sarif(
|
|
|
319
341
|
|
|
320
342
|
fingerprints = {"lsaFindingId": f.get("finding_id", f"LSA-{len(results)+1}")}
|
|
321
343
|
if f.get("fingerprint"):
|
|
322
|
-
fingerprints["
|
|
344
|
+
fingerprints["komagu/v1"] = f["fingerprint"]
|
|
323
345
|
|
|
324
346
|
result_taxa = []
|
|
325
347
|
for c in norm_cwes:
|
|
@@ -352,6 +374,7 @@ def to_sarif(
|
|
|
352
374
|
"confidence": f.get("confidence", "high"),
|
|
353
375
|
"status": f.get("status", "unverified"),
|
|
354
376
|
"cwe": norm_cwes,
|
|
377
|
+
**replay_properties(f),
|
|
355
378
|
},
|
|
356
379
|
}
|
|
357
380
|
state = f.get("baseline_state")
|
|
@@ -93,6 +93,17 @@ class TestCodexAdapter(unittest.TestCase):
|
|
|
93
93
|
self.assertEqual(text.count(AGENTS_MD_BEGIN), 1)
|
|
94
94
|
self.assertEqual(text.count(AGENTS_MD_END), 1)
|
|
95
95
|
|
|
96
|
+
def test_pre_2_0_markers_are_replaced_in_place(self) -> None:
|
|
97
|
+
old = (f"# Notes\n\n{build_adapters.LEGACY_AGENTS_MD_BEGIN}\nold block\n"
|
|
98
|
+
f"{build_adapters.LEGACY_AGENTS_MD_END}\n\n# Footer\n")
|
|
99
|
+
(self.dest / "AGENTS.md").write_text(old, encoding="utf-8")
|
|
100
|
+
build("codex", self.dest)
|
|
101
|
+
text = (self.dest / "AGENTS.md").read_text(encoding="utf-8")
|
|
102
|
+
self.assertNotIn("komainu-lsa", text)
|
|
103
|
+
self.assertNotIn("old block", text)
|
|
104
|
+
self.assertEqual(text.count(AGENTS_MD_BEGIN), 1)
|
|
105
|
+
self.assertTrue(text.startswith("# Notes\n\n") and text.endswith("# Footer\n"))
|
|
106
|
+
|
|
96
107
|
def test_unbalanced_markers_fail_before_touching_workspace(self) -> None:
|
|
97
108
|
user = f"# Notes\n{AGENTS_MD_BEGIN}\nimportant user text\n"
|
|
98
109
|
(self.dest / "AGENTS.md").write_text(user, encoding="utf-8")
|
|
@@ -187,7 +198,7 @@ class TestGenericAdapterGuard(unittest.TestCase):
|
|
|
187
198
|
|
|
188
199
|
def test_contract_carries_generator_marker(self) -> None:
|
|
189
200
|
build("generic", self.dest)
|
|
190
|
-
self.assertEqual(json.loads(self.contract.read_text(encoding="utf-8"))["generator"], "
|
|
201
|
+
self.assertEqual(json.loads(self.contract.read_text(encoding="utf-8"))["generator"], "komagu-lsa")
|
|
191
202
|
|
|
192
203
|
def test_rebuild_refreshes_generated_contract(self) -> None:
|
|
193
204
|
build("generic", self.dest)
|
|
@@ -197,6 +208,14 @@ class TestGenericAdapterGuard(unittest.TestCase):
|
|
|
197
208
|
build("generic", self.dest)
|
|
198
209
|
self.assertEqual(json.loads(self.contract.read_text(encoding="utf-8"))["kit_version"], build_adapters.VERSION)
|
|
199
210
|
|
|
211
|
+
def test_rebuild_accepts_pre_2_0_generator(self) -> None:
|
|
212
|
+
build("generic", self.dest)
|
|
213
|
+
data = json.loads(self.contract.read_text(encoding="utf-8"))
|
|
214
|
+
data["generator"] = "komainu-lsa"
|
|
215
|
+
self.contract.write_text(json.dumps(data), encoding="utf-8")
|
|
216
|
+
build("generic", self.dest)
|
|
217
|
+
self.assertEqual(json.loads(self.contract.read_text(encoding="utf-8"))["generator"], "komagu-lsa")
|
|
218
|
+
|
|
200
219
|
def test_foreign_contract_fails_before_touching_workspace(self) -> None:
|
|
201
220
|
self.contract.write_text('{"name": "my-project-contract"}', encoding="utf-8")
|
|
202
221
|
with self.assertRaisesRegex(ValueError, "contract.json"):
|
|
@@ -211,7 +230,7 @@ class TestGenericAdapterGuard(unittest.TestCase):
|
|
|
211
230
|
self.assert_untouched("contract.json")
|
|
212
231
|
|
|
213
232
|
def test_non_object_contract_fails(self) -> None:
|
|
214
|
-
self.contract.write_text('["
|
|
233
|
+
self.contract.write_text('["komagu-lsa"]', encoding="utf-8")
|
|
215
234
|
with self.assertRaises(ValueError):
|
|
216
235
|
build("generic", self.dest)
|
|
217
236
|
|
|
@@ -198,6 +198,14 @@ class TestLoadBaseline(unittest.TestCase):
|
|
|
198
198
|
entries = load_baseline(self.dir / "prev.sarif")
|
|
199
199
|
self.assertEqual(entries["kfp1:" + "e" * 64]["state"], "open")
|
|
200
200
|
|
|
201
|
+
def test_loads_pre_2_0_sarif_fingerprint_key(self) -> None:
|
|
202
|
+
sarif = to_sarif({"findings": [{**_finding(), "fingerprint": "kfp1:" + "f" * 64}]})
|
|
203
|
+
for result in sarif["runs"][0]["results"]:
|
|
204
|
+
fps = result["partialFingerprints"]
|
|
205
|
+
fps["komainu/v1"] = fps.pop("komagu/v1")
|
|
206
|
+
write_json(self.dir / "old.sarif", sarif)
|
|
207
|
+
self.assertIn("kfp1:" + "f" * 64, load_baseline(self.dir / "old.sarif"))
|
|
208
|
+
|
|
201
209
|
def test_invalid_file_raises(self) -> None:
|
|
202
210
|
(self.dir / "bad.json").write_text("{not json", encoding="utf-8")
|
|
203
211
|
with self.assertRaises(BaselineError):
|
|
@@ -156,9 +156,9 @@ class TestLsaSarifExport(unittest.TestCase):
|
|
|
156
156
|
by_rule = {r["ruleId"]: r for r in self.run["results"]}
|
|
157
157
|
mass_fp = by_rule["lsa/mass_assignment/whole_object_binding"]["partialFingerprints"]
|
|
158
158
|
bfla_fp = by_rule["lsa/bfla/missing_role_check"]["partialFingerprints"]
|
|
159
|
-
self.assertEqual(mass_fp["
|
|
159
|
+
self.assertEqual(mass_fp["komagu/v1"], FINGERPRINT)
|
|
160
160
|
self.assertEqual(mass_fp["lsaFindingId"], "MASS-R-0002-1")
|
|
161
|
-
self.assertNotIn("
|
|
161
|
+
self.assertNotIn("komagu/v1", bfla_fp)
|
|
162
162
|
|
|
163
163
|
|
|
164
164
|
class TestOperateSarifExport(unittest.TestCase):
|
|
@@ -166,16 +166,16 @@ class TestOperateSarifExport(unittest.TestCase):
|
|
|
166
166
|
sarif = convert_findings_to_sarif([_finding(cwe=285), {**_mass_assignment(), "cwe": 915}])
|
|
167
167
|
run = sarif["runs"][0]
|
|
168
168
|
rule_ids = {r["id"] for r in run["tool"]["driver"]["rules"]}
|
|
169
|
-
self.assertEqual(rule_ids, {"
|
|
170
|
-
mass = next(r for r in run["results"] if r["ruleId"] == "
|
|
171
|
-
self.assertEqual(mass["partialFingerprints"], {"
|
|
172
|
-
bfla = next(r for r in run["results"] if r["ruleId"] == "
|
|
169
|
+
self.assertEqual(rule_ids, {"KOMAGU-BFLA-CWE285", "KOMAGU-MASS-CWE915"})
|
|
170
|
+
mass = next(r for r in run["results"] if r["ruleId"] == "KOMAGU-MASS-CWE915")
|
|
171
|
+
self.assertEqual(mass["partialFingerprints"], {"komagu/v1": FINGERPRINT})
|
|
172
|
+
bfla = next(r for r in run["results"] if r["ruleId"] == "KOMAGU-BFLA-CWE285")
|
|
173
173
|
self.assertNotIn("partialFingerprints", bfla)
|
|
174
174
|
|
|
175
175
|
def test_default_cwe_for_new_categories(self) -> None:
|
|
176
176
|
sarif = convert_findings_to_sarif([_finding(cwe=None), {**_mass_assignment(), "cwe": None}])
|
|
177
177
|
rule_ids = {r["id"] for r in sarif["runs"][0]["tool"]["driver"]["rules"]}
|
|
178
|
-
self.assertEqual(rule_ids, {"
|
|
178
|
+
self.assertEqual(rule_ids, {"KOMAGU-BFLA-CWE285", "KOMAGU-MASS-CWE915"})
|
|
179
179
|
|
|
180
180
|
|
|
181
181
|
class TestSummaryAndReports(unittest.TestCase):
|
package/operate/cli/__init__.py
CHANGED
|
@@ -165,13 +165,8 @@ Examples:
|
|
|
165
165
|
|
|
166
166
|
|
|
167
167
|
def _default_suppressions_file(target_path: Path) -> Path:
|
|
168
|
-
"""TARGET/.komagu/suppressions.json
|
|
169
|
-
|
|
170
|
-
legacy = target_path / ".komainu" / "suppressions.json"
|
|
171
|
-
if not current.is_file() and legacy.is_file():
|
|
172
|
-
print(f"warning: {legacy} is deprecated, move it to {current}", file=sys.stderr)
|
|
173
|
-
return legacy
|
|
174
|
-
return current
|
|
168
|
+
"""TARGET/.komagu/suppressions.json."""
|
|
169
|
+
return target_path / ".komagu" / "suppressions.json"
|
|
175
170
|
|
|
176
171
|
|
|
177
172
|
def execute_scan(args: argparse.Namespace) -> int:
|
|
@@ -297,7 +297,7 @@ def execute_test(args: argparse.Namespace) -> int:
|
|
|
297
297
|
|
|
298
298
|
|
|
299
299
|
def _matrix_cases_from_scan(scan_file: Path, args: argparse.Namespace, min_consensus: float) -> List[MatrixCase]:
|
|
300
|
-
"""BFLA and mass assignment findings from findings.json
|
|
300
|
+
"""BFLA and mass assignment findings from findings.json or SARIF."""
|
|
301
301
|
try:
|
|
302
302
|
data = json.loads(scan_file.read_text(encoding="utf-8"))
|
|
303
303
|
except (OSError, ValueError):
|
package/operate/cli/env.py
CHANGED
|
@@ -1,30 +1,14 @@
|
|
|
1
|
-
"""Environment variable lookup
|
|
1
|
+
"""Environment variable lookup for KOMAGU_* settings."""
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import os
|
|
6
|
-
import
|
|
7
|
-
from typing import Mapping, Optional, Set
|
|
6
|
+
from typing import Mapping, Optional
|
|
8
7
|
|
|
9
8
|
PREFIX = "KOMAGU_"
|
|
10
|
-
LEGACY_PREFIX = "KOMAINU_"
|
|
11
|
-
|
|
12
|
-
_warned: Set[str] = set()
|
|
13
9
|
|
|
14
10
|
|
|
15
11
|
def getenv(name: str, default: Optional[str] = None, env: Optional[Mapping[str, str]] = None) -> Optional[str]:
|
|
16
|
-
"""Read KOMAGU_<name
|
|
12
|
+
"""Read KOMAGU_<name>."""
|
|
17
13
|
env = os.environ if env is None else env
|
|
18
|
-
|
|
19
|
-
if value is not None:
|
|
20
|
-
return value
|
|
21
|
-
legacy = env.get(LEGACY_PREFIX + name)
|
|
22
|
-
if legacy is not None:
|
|
23
|
-
if name not in _warned:
|
|
24
|
-
_warned.add(name)
|
|
25
|
-
print(
|
|
26
|
-
f"warning: {LEGACY_PREFIX}{name} is deprecated, set {PREFIX}{name} instead",
|
|
27
|
-
file=sys.stderr,
|
|
28
|
-
)
|
|
29
|
-
return legacy
|
|
30
|
-
return default
|
|
14
|
+
return env.get(PREFIX + name, default)
|
|
@@ -27,6 +27,8 @@ import urllib.request
|
|
|
27
27
|
from dataclasses import dataclass, field
|
|
28
28
|
from typing import Any, Dict, List, Mapping, Optional, Tuple
|
|
29
29
|
|
|
30
|
+
from operate.lsa_sarif.sarif_findings import findings_from_sarif, is_sarif
|
|
31
|
+
|
|
30
32
|
SUITE_VERSION = 2
|
|
31
33
|
DENY_STATUS = (401, 403, 404)
|
|
32
34
|
STRICT_DENY_STATUS = (404,)
|
|
@@ -278,12 +280,14 @@ def generate_matrix_cases_from_findings(
|
|
|
278
280
|
default_id: str = "12345",
|
|
279
281
|
min_consensus: float = 0.0,
|
|
280
282
|
) -> List[MatrixCase]:
|
|
281
|
-
"""Turn BFLA and mass assignment findings into matrix cases.
|
|
283
|
+
"""Turn BFLA and mass assignment findings (findings.json or SARIF) into matrix cases.
|
|
282
284
|
|
|
283
285
|
BFLA findings are replayed by a normal user (``token_env``) who must be
|
|
284
286
|
refused. Mass assignment findings are probed by the same user with the
|
|
285
287
|
privileged fields from ``attack_vector.fields`` (default: role / is_admin).
|
|
286
288
|
"""
|
|
289
|
+
if is_sarif(data):
|
|
290
|
+
data = findings_from_sarif(data)
|
|
287
291
|
items = data if isinstance(data, list) else (data.get("findings", []) if isinstance(data, dict) else [])
|
|
288
292
|
persona = Persona(name="user", token_env=token_env)
|
|
289
293
|
cases: List[MatrixCase] = []
|
|
@@ -107,14 +107,14 @@ class DevSecOpsPipeline:
|
|
|
107
107
|
results = []
|
|
108
108
|
rules = [
|
|
109
109
|
{
|
|
110
|
-
"id": "
|
|
110
|
+
"id": "KOMAGU-SECRET-001",
|
|
111
111
|
"name": "HardcodedCredentialDetected",
|
|
112
112
|
"shortDescription": {"text": "Potential hardcoded secret or API credential detected."},
|
|
113
113
|
"defaultConfiguration": {"level": "error"},
|
|
114
114
|
}
|
|
115
115
|
]
|
|
116
116
|
|
|
117
|
-
skip_dirs = {".git", "node_modules", "bin", "obj", ".komagu", ".komagu-devsecops", "
|
|
117
|
+
skip_dirs = {".git", "node_modules", "bin", "obj", ".komagu", ".komagu-devsecops", "dist", "build"}
|
|
118
118
|
skip_exts = {".png", ".jpg", ".jpeg", ".gif", ".ico", ".pdf", ".zip", ".tar", ".gz", ".db", ".sqlite"}
|
|
119
119
|
|
|
120
120
|
for dirpath, dirnames, filenames in os.walk(self.target_dir):
|
|
@@ -137,7 +137,7 @@ class DevSecOpsPipeline:
|
|
|
137
137
|
if re.search(pattern, line):
|
|
138
138
|
results.append(
|
|
139
139
|
{
|
|
140
|
-
"ruleId": "
|
|
140
|
+
"ruleId": "KOMAGU-SECRET-001",
|
|
141
141
|
"level": "error",
|
|
142
142
|
"message": {"text": f"{desc} found in {rel_path}"},
|
|
143
143
|
"locations": [
|
|
@@ -324,7 +324,7 @@ class DevSecOpsPipeline:
|
|
|
324
324
|
results = []
|
|
325
325
|
rules = [
|
|
326
326
|
{
|
|
327
|
-
"id": "
|
|
327
|
+
"id": "KOMAGU-CONTAINER-001",
|
|
328
328
|
"name": "DockerfileSecurityMisconfiguration",
|
|
329
329
|
"shortDescription": {"text": "Container configuration deviates from production hardening standards."},
|
|
330
330
|
"defaultConfiguration": {"level": "warning"},
|
|
@@ -343,7 +343,7 @@ class DevSecOpsPipeline:
|
|
|
343
343
|
if re.search(pattern, line.strip()):
|
|
344
344
|
results.append(
|
|
345
345
|
{
|
|
346
|
-
"ruleId": "
|
|
346
|
+
"ruleId": "KOMAGU-CONTAINER-001",
|
|
347
347
|
"level": level,
|
|
348
348
|
"message": {"text": f"{msg} at {rel_path}:{line_no}"},
|
|
349
349
|
"locations": [
|
|
@@ -15,6 +15,8 @@ import re
|
|
|
15
15
|
from pathlib import Path
|
|
16
16
|
from typing import Any, Dict, List, Optional, Union
|
|
17
17
|
|
|
18
|
+
from operate.lsa_sarif.sarif_findings import findings_from_sarif, is_sarif
|
|
19
|
+
|
|
18
20
|
|
|
19
21
|
class PoCArtifact:
|
|
20
22
|
"""Represents a generated Proof-of-Concept exploit artifact."""
|
|
@@ -317,7 +319,9 @@ curl -i -G "{self.base_url}{path}" \\
|
|
|
317
319
|
self,
|
|
318
320
|
findings_data: Union[Dict[str, Any], List[Dict[str, Any]]],
|
|
319
321
|
) -> List[PoCArtifact]:
|
|
320
|
-
"""Generate PoC artifacts for all confirmed/likely findings."""
|
|
322
|
+
"""Generate PoC artifacts for all confirmed/likely findings (findings.json or SARIF)."""
|
|
323
|
+
if is_sarif(findings_data):
|
|
324
|
+
findings_data = findings_from_sarif(findings_data)
|
|
321
325
|
items = findings_data if isinstance(findings_data, list) else findings_data.get("findings", [])
|
|
322
326
|
artifacts: List[PoCArtifact] = []
|
|
323
327
|
|
|
@@ -69,7 +69,28 @@ RULE_ID_PREFIX = {"bola": "BOLA", "bfla": "BFLA", "mass_assignment": "MASS"}
|
|
|
69
69
|
|
|
70
70
|
def _get_rule_id(category: str, cwe: int) -> str:
|
|
71
71
|
cat_prefix = RULE_ID_PREFIX.get(category, "INJ")
|
|
72
|
-
return f"
|
|
72
|
+
return f"KOMAGU-{cat_prefix}-CWE{cwe}"
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
# Same replay properties as assess/lsa/scripts/findings_to_sarif.py, so `komagu test --from-scan`
|
|
76
|
+
# and `--export-poc` can read either exporter's SARIF. suggested_payload is left out on purpose.
|
|
77
|
+
_REPLAY_KEYS = ("method", "path", "fields", "readback", "target_param")
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _replay_properties(finding: Dict[str, Any]) -> Dict[str, Any]:
|
|
81
|
+
props: Dict[str, Any] = {}
|
|
82
|
+
ep = finding.get("endpoint")
|
|
83
|
+
if isinstance(ep, dict) and ep.get("path"):
|
|
84
|
+
props["endpoint"] = {k: ep[k] for k in ("method", "path") if ep.get(k)}
|
|
85
|
+
av = finding.get("attack_vector")
|
|
86
|
+
if isinstance(av, dict):
|
|
87
|
+
kept = {k: av[k] for k in _REPLAY_KEYS if av.get(k) not in (None, "", {})}
|
|
88
|
+
if kept:
|
|
89
|
+
props["attack_vector"] = kept
|
|
90
|
+
cons = finding.get("consensus")
|
|
91
|
+
if isinstance(cons, dict) and isinstance(cons.get("score"), (int, float)):
|
|
92
|
+
props["consensus_score"] = cons["score"]
|
|
93
|
+
return props
|
|
73
94
|
|
|
74
95
|
|
|
75
96
|
def convert_findings_to_sarif(
|
|
@@ -172,6 +193,7 @@ def convert_findings_to_sarif(
|
|
|
172
193
|
"confidence": finding.get("confidence"),
|
|
173
194
|
"cwe": cwe,
|
|
174
195
|
"status": finding.get("status", "new"),
|
|
196
|
+
**_replay_properties(finding),
|
|
175
197
|
},
|
|
176
198
|
}
|
|
177
199
|
|
|
@@ -179,7 +201,7 @@ def convert_findings_to_sarif(
|
|
|
179
201
|
result_item["relatedLocations"] = related_locations
|
|
180
202
|
|
|
181
203
|
if finding.get("fingerprint"):
|
|
182
|
-
result_item["partialFingerprints"] = {"
|
|
204
|
+
result_item["partialFingerprints"] = {"komagu/v1": finding["fingerprint"]}
|
|
183
205
|
|
|
184
206
|
results.append(result_item)
|
|
185
207
|
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Read LSA SARIF results back as findings.json-style entries (#289).
|
|
3
|
+
|
|
4
|
+
`komagu test --from-scan` and `--export-poc` work on findings.json entries. This lets them take
|
|
5
|
+
the SARIF that `komagu scan` writes instead. Both LSA exporters (assess/lsa/scripts/findings_to_sarif.py
|
|
6
|
+
and operate/lsa_sarif/lsa_to_sarif.py) put finding_id, category, endpoint, attack_vector and
|
|
7
|
+
consensus_score in each result's properties; older SARIF without them falls back to the rule id,
|
|
8
|
+
the OWASP rule tags and the "[METHOD /path]" part of the message.
|
|
9
|
+
|
|
10
|
+
Python standard library only.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import re
|
|
16
|
+
from typing import Any, Dict, List, Optional
|
|
17
|
+
|
|
18
|
+
# OWASP API Top 10 rule tags written by findings_to_sarif.py, for SARIF whose ruleId has no category
|
|
19
|
+
_TAG_CATEGORY = {
|
|
20
|
+
"owasp-api1:2023-bola": "bola",
|
|
21
|
+
"owasp-api5:2023-bfla": "bfla",
|
|
22
|
+
"owasp-api3:2023-bopla": "mass_assignment",
|
|
23
|
+
}
|
|
24
|
+
# Rule id prefixes written by operate/lsa_sarif/lsa_to_sarif.py (KOMAGU-<PREFIX>-CWE<n>;
|
|
25
|
+
# 1.x SARIF files use KOMAINU-, still read for one major release, #312)
|
|
26
|
+
_LEGACY_PREFIX_CATEGORY = {"BOLA": "bola", "BFLA": "bfla", "MASS": "mass_assignment", "INJ": "sql_injection"}
|
|
27
|
+
_ENDPOINT_IN_MESSAGE = re.compile(r"\[(GET|POST|PUT|PATCH|DELETE)(?:\|[A-Z]+)*\s+(/[^\]\s]*)\]")
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def is_sarif(data: Any) -> bool:
|
|
31
|
+
return isinstance(data, dict) and isinstance(data.get("runs"), list)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _rule_tags(run: Dict[str, Any]) -> Dict[str, List[str]]:
|
|
35
|
+
rules = ((run.get("tool") or {}).get("driver") or {}).get("rules") or []
|
|
36
|
+
return {
|
|
37
|
+
str(r.get("id", "")): [str(t).lower() for t in ((r.get("properties") or {}).get("tags") or [])]
|
|
38
|
+
for r in rules
|
|
39
|
+
if isinstance(r, dict)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _category(rule_id: str, props: Dict[str, Any], tags: List[str]) -> Optional[str]:
|
|
44
|
+
if props.get("category"):
|
|
45
|
+
return str(props["category"]).lower()
|
|
46
|
+
if rule_id.startswith("lsa/"):
|
|
47
|
+
parts = rule_id.split("/")
|
|
48
|
+
if len(parts) > 1 and parts[1]:
|
|
49
|
+
return parts[1].lower()
|
|
50
|
+
m = re.match(r"(?:KOMAINU|KOMAGU)-([A-Z]+)-", rule_id)
|
|
51
|
+
if m and m.group(1) in _LEGACY_PREFIX_CATEGORY:
|
|
52
|
+
return _LEGACY_PREFIX_CATEGORY[m.group(1)]
|
|
53
|
+
for tag in tags:
|
|
54
|
+
if tag in _TAG_CATEGORY:
|
|
55
|
+
return _TAG_CATEGORY[tag]
|
|
56
|
+
return None
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def findings_from_sarif(data: Dict[str, Any]) -> List[Dict[str, Any]]:
|
|
60
|
+
"""Return one findings.json-style dict per SARIF result that names an endpoint."""
|
|
61
|
+
findings: List[Dict[str, Any]] = []
|
|
62
|
+
for run in data.get("runs") or []:
|
|
63
|
+
if not isinstance(run, dict):
|
|
64
|
+
continue
|
|
65
|
+
tags_by_rule = _rule_tags(run)
|
|
66
|
+
for result in run.get("results") or []:
|
|
67
|
+
if not isinstance(result, dict):
|
|
68
|
+
continue
|
|
69
|
+
rule_id = str(result.get("ruleId", ""))
|
|
70
|
+
props = result.get("properties") or {}
|
|
71
|
+
category = _category(rule_id, props, tags_by_rule.get(rule_id, []))
|
|
72
|
+
if category is None:
|
|
73
|
+
continue
|
|
74
|
+
message = str((result.get("message") or {}).get("text", ""))
|
|
75
|
+
endpoint = props.get("endpoint") if isinstance(props.get("endpoint"), dict) else None
|
|
76
|
+
if not endpoint:
|
|
77
|
+
m = _ENDPOINT_IN_MESSAGE.search(message)
|
|
78
|
+
endpoint = {"method": m.group(1), "path": m.group(2)} if m else None
|
|
79
|
+
attack_vector = props.get("attack_vector") if isinstance(props.get("attack_vector"), dict) else None
|
|
80
|
+
if not endpoint and not (attack_vector and attack_vector.get("path")):
|
|
81
|
+
continue
|
|
82
|
+
finding_id = props.get("finding_id") or (result.get("partialFingerprints") or {}).get("lsaFindingId") or rule_id
|
|
83
|
+
finding: Dict[str, Any] = {
|
|
84
|
+
"finding_id": str(finding_id),
|
|
85
|
+
"category": category,
|
|
86
|
+
"title": message.split(" [", 1)[0] or rule_id,
|
|
87
|
+
"status": props.get("status", "confirmed"),
|
|
88
|
+
}
|
|
89
|
+
if endpoint:
|
|
90
|
+
finding["endpoint"] = dict(endpoint)
|
|
91
|
+
if attack_vector:
|
|
92
|
+
finding["attack_vector"] = dict(attack_vector)
|
|
93
|
+
if isinstance(props.get("consensus_score"), (int, float)):
|
|
94
|
+
finding["consensus"] = {"score": props["consensus_score"]}
|
|
95
|
+
findings.append(finding)
|
|
96
|
+
return findings
|