@hoanghainh/komagu 2.3.0 → 2.5.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 +36 -0
- package/LICENSE +202 -0
- package/README.md +1 -0
- package/assess/lsa/CHANGELOG.md +15 -0
- package/assess/lsa/scripts/detect_stack.py +15 -6
- package/assess/lsa/scripts/find_injection.py +35 -2
- package/assess/lsa/scripts/java_routes.py +323 -0
- package/assess/lsa/scripts/lsa_common.py +73 -7
- package/assess/lsa/scripts/map_routes.py +36 -160
- package/assess/lsa/scripts/merge_findings.py +2 -0
- package/assess/lsa/scripts/redaction.py +118 -0
- package/assess/lsa/tests/test_java_legacy_realistic.py +453 -0
- package/assess/lsa/tests/test_modern_stacks.py +13 -0
- package/contracts/schemas/persona-suite.schema.json +38 -2
- package/examples/member-portal/backend-dotnet/Dockerfile +2 -1
- package/examples/member-portal/backend-spring/Dockerfile +2 -0
- package/examples/member-portal/docker-compose.yml +1 -1
- package/examples/member-portal/frontend/Dockerfile +3 -2
- package/examples/member-portal/frontend/package-lock.json +24 -656
- package/examples/member-portal/frontend/package.json +1 -1
- package/operate/ai/context.py +22 -2
- package/operate/ai/runner.py +20 -2
- package/operate/cli/__init__.py +1 -1
- package/operate/cli/commands/fix.py +4 -0
- package/operate/cli/commands/leak_check.py +75 -0
- package/operate/cli/commands/protect.py +32 -3
- package/operate/cli/commands/test.py +131 -15
- package/operate/cli/main.py +5 -3
- package/operate/cross_user_tester/persona_matrix.py +118 -25
- package/operate/cross_user_tester/runner.py +190 -7
- package/operate/cross_user_tester/session_config.py +180 -0
- package/operate/cross_user_tester/session_http.py +288 -0
- package/operate/devsecops/leak_check.py +314 -0
- package/operate/remediator/runner.py +18 -1
- package/operate/remediator/strategies.py +2 -2
- package/operate/simulate/scenarios.py +2 -2
- package/operate/tests/mock_legacy_session_app.py +160 -0
- package/operate/tests/test_ai_redaction.py +181 -0
- package/operate/tests/test_cross_user_session.py +565 -0
- package/operate/tests/test_docker_packaging.py +3 -3
- package/operate/tests/test_headless_ai.py +22 -1
- package/operate/tests/test_leak_check.py +250 -0
- package/operate/tests/test_legacy_session_edge.py +192 -0
- package/operate/tests/test_pilot_safeguards.py +245 -0
- package/operate/tests/test_remediator_java_legacy.py +330 -0
- package/package.json +2 -1
- package/remediate/authz/patcher.py +120 -11
- package/remediate/authz/tests/test_bola_patcher.py +4 -1
- package/remediate/edge/coraza/Caddyfile +1 -1
- package/remediate/edge/coraza/Dockerfile +9 -0
- package/remediate/edge/coraza/rules/coraza-crs-custom.conf +12 -0
- package/remediate/edge/coraza/virtual_patcher.py +24 -0
- package/remediate/edge/docker-compose.yml +7 -2
- package/remediate/edge/kong/kong-legacy-session.yaml +51 -0
- package/remediate/edge/mock-backend/Dockerfile +2 -0
- package/remediate/edge/tests/test_edge_security.py +21 -1
- package/remediate/resilience/terraform/backup_worm.tf +39 -1
- package/remediate/resilience/terraform/main.tf +15 -3
- package/remediate/resilience/terraform/variables.tf +11 -0
- package/remediate/resilience/tests/test_resilience.py +16 -0
|
@@ -19,8 +19,11 @@ from pathlib import Path
|
|
|
19
19
|
from typing import Dict, List, Optional, Tuple
|
|
20
20
|
|
|
21
21
|
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
|
22
|
-
from
|
|
23
|
-
|
|
22
|
+
from java_routes import (build_class_index, collect_spring_beans, entry_method_line, java_declared, # noqa: E402
|
|
23
|
+
struts2_action_extension,
|
|
24
|
+
struts_modules, xml_declared)
|
|
25
|
+
from lsa_common import (JAVA_WEB_ROOT, KIT_VERSION, call_args, compile_id_patterns, is_id_name, # noqa: E402
|
|
26
|
+
iter_source_files, line_of, load_config, read_text, split_top, write_json)
|
|
24
27
|
from nextjs_routes import middleware_note, nextjs_declared # noqa: E402
|
|
25
28
|
from privilege import annotate_privilege # noqa: E402
|
|
26
29
|
|
|
@@ -250,8 +253,6 @@ def segments(text: str, decl_rx: re.Pattern) -> List[Tuple[re.Match, int, int]]:
|
|
|
250
253
|
# Declared-route extractors
|
|
251
254
|
# --------------------------------------------------------------------------------------
|
|
252
255
|
PHP_METHOD = re.compile(r"^[ \t]*(?:(public|protected|private)\s+)?(?:static\s+)?function\s+&?(\w+)\s*\(", re.M)
|
|
253
|
-
JAVA_METHOD_ANN = re.compile(r"@(RequestMapping|GetMapping|PostMapping|PutMapping|DeleteMapping|PatchMapping|Path)\b\s*(\(([^)]*)\))?")
|
|
254
|
-
JAVA_HTTP_ANN = re.compile(r"@(GET|POST|PUT|DELETE|PATCH)\b")
|
|
255
256
|
CS_ACTION = re.compile(r"^[ \t]*public\s+(?!record\b|class\b)(?:virtual\s+|async\s+|override\s+|static\s+)*[A-Za-z0-9_<>., \[\]?]+\s+(\w+)\s*\(([^)]*)\)", re.M)
|
|
256
257
|
VB_ACTION = re.compile(r"^[ \t]*Public\s+(?:Overridable\s+|Async\s+)*Function\s+(\w+)\s*\(([^)]*)\)\s+As\s+[\w.(<>) ]+", re.M | re.I)
|
|
257
258
|
WEBMETHOD = re.compile(r"(\[WebMethod[^\]]*\]|<WebMethod[^>]*>|\[OperationContract[^\]]*\]|<OperationContract[^>]*>)[\s\S]{0,300}?"
|
|
@@ -305,107 +306,6 @@ def php_declared(c: Collector, rel: str, text: str, stacks: set) -> bool:
|
|
|
305
306
|
return found
|
|
306
307
|
|
|
307
308
|
|
|
308
|
-
def java_declared(c: Collector, rel: str, text: str) -> bool:
|
|
309
|
-
found = False
|
|
310
|
-
cls_prefix = ""
|
|
311
|
-
cm = re.search(r"@(RequestMapping|Path)\s*\(\s*(?:value\s*=\s*)?\"([^\"]*)\"[^)]*\)\s*(?:@\w+(?:\([^)]*\))?\s*)*(?:public\s+)?(?:abstract\s+)?class", text)
|
|
312
|
-
if cm:
|
|
313
|
-
cls_prefix = cm.group(2)
|
|
314
|
-
is_servlet = re.search(r"extends\s+HttpServlet", text)
|
|
315
|
-
if is_servlet:
|
|
316
|
-
ws = re.search(r"@WebServlet\s*\(\s*(?:urlPatterns\s*=\s*|value\s*=\s*)?\{?\s*\"([^\"]+)\"", text)
|
|
317
|
-
for m in re.finditer(r"(?:public|protected)\s+void\s+(doGet|doPost|doPut|doDelete|service)\s*\(", text):
|
|
318
|
-
start = m.start()
|
|
319
|
-
nxt = re.search(r"\n\s*(?:public|protected|private)\s+[\w<>\[\]]+\s+\w+\s*\(", text[m.end():])
|
|
320
|
-
body = text[start: m.end() + (nxt.start() if nxt else len(text))]
|
|
321
|
-
line = line_of(text, start)
|
|
322
|
-
verb = {"doGet": "GET", "doPost": "POST", "doPut": "PUT", "doDelete": "DELETE"}.get(m.group(1), "ANY")
|
|
323
|
-
c.add(stack="java-servlet", kind="declared_route", method=[verb],
|
|
324
|
-
path=ws.group(1) if ws else "<see web.xml servlet-mapping>",
|
|
325
|
-
handler={"file": rel, "line": line, "symbol": m.group(1)},
|
|
326
|
-
id_params=c.inputs("java", body, line), guard_hints=c.guards(body, line),
|
|
327
|
-
data_hints=c.data_ops(body, line), notes="" if ws else "resolve path from web.xml")
|
|
328
|
-
found = True
|
|
329
|
-
return found
|
|
330
|
-
anns = list(JAVA_METHOD_ANN.finditer(text))
|
|
331
|
-
for m in anns:
|
|
332
|
-
if cm and m.start() == cm.start():
|
|
333
|
-
continue
|
|
334
|
-
after = text[m.end(): m.end() + 600]
|
|
335
|
-
# skip class-level annotation
|
|
336
|
-
if re.match(r"\s*(?:@\w+(?:\([^)]*\))?\s*)*(?:public\s+)?(?:abstract\s+)?class\b", after):
|
|
337
|
-
continue
|
|
338
|
-
sig = re.search(r"(?:(?:public|protected)\s+[\w<>\[\],.? ]+|fun|(?:override|suspend)\s+fun)\s+(\w+)\s*\(", after)
|
|
339
|
-
if not sig:
|
|
340
|
-
continue
|
|
341
|
-
before = text[:m.start()]
|
|
342
|
-
cls_m = list(re.finditer(r"@(?:RequestMapping|Path)\s*\(\s*(?:value\s*=\s*|path\s*=\s*)?\"([^\"]*)\"[^)]*\)[\s\S]{0,300}?(?:class\b)", before))
|
|
343
|
-
method_cls_prefix = cls_m[-1].group(1) if cls_m else cls_prefix
|
|
344
|
-
args = m.group(3) or ""
|
|
345
|
-
pv = re.search(r"(?:value\s*=\s*|path\s*=\s*)?\{?\s*\"([^\"]*)\"", args)
|
|
346
|
-
sub = pv.group(1) if pv else ""
|
|
347
|
-
verbs = re.findall(r"RequestMethod\.(\w+)", args)
|
|
348
|
-
ann = m.group(1)
|
|
349
|
-
if ann.endswith("Mapping") and ann != "RequestMapping":
|
|
350
|
-
verbs = [ann[:-7].upper()]
|
|
351
|
-
if ann == "Path":
|
|
352
|
-
hv = JAVA_HTTP_ANN.search(text[max(0, m.start() - 200): m.end() + 200])
|
|
353
|
-
verbs = [hv.group(1)] if hv else ["ANY"]
|
|
354
|
-
path = "/" + "/".join(p.strip("/") for p in (method_cls_prefix, sub) if p.strip("/"))
|
|
355
|
-
nxt = re.search(r"\n\s*@(RequestMapping|GetMapping|PostMapping|PutMapping|DeleteMapping|PatchMapping|Path)\b(?!Variable|Param)", text[m.end():])
|
|
356
|
-
body = text[m.start(): m.end() + (nxt.start() if nxt else len(text))]
|
|
357
|
-
line = line_of(text, m.start())
|
|
358
|
-
c.add(stack="java-spring-mvc" if ann != "Path" else "java-jaxrs", kind="declared_route", method=verbs or ["ANY"],
|
|
359
|
-
path=path, handler={"file": rel, "line": line, "symbol": sig.group(1)},
|
|
360
|
-
id_params=c.path_params(path) + c.inputs("java", body, line),
|
|
361
|
-
guard_hints=c.guards(text[:m.start()][-1500:], 1)[:2] + c.guards(body, line),
|
|
362
|
-
data_hints=c.data_ops(body, line), notes="")
|
|
363
|
-
found = True
|
|
364
|
-
# Struts 1 / 2 Action classes without annotations are mapped from XML (see xml_declared)
|
|
365
|
-
return found
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
def xml_declared(c: Collector, rel: str, text: str, class_index: Dict[str, str]) -> bool:
|
|
369
|
-
found = False
|
|
370
|
-
if rel.endswith("web.xml"):
|
|
371
|
-
servlets = dict(re.findall(r"<servlet>\s*<servlet-name>\s*([^<]+?)\s*</servlet-name>[\s\S]*?<servlet-class>\s*([^<]+?)\s*</servlet-class>", text))
|
|
372
|
-
for m in re.finditer(r"<servlet-mapping>\s*<servlet-name>\s*([^<]+?)\s*</servlet-name>\s*<url-pattern>\s*([^<]+?)\s*</url-pattern>", text):
|
|
373
|
-
name, pattern = m.group(1), m.group(2)
|
|
374
|
-
cls = servlets.get(name, "")
|
|
375
|
-
hfile = class_index.get(cls.split(".")[-1], rel)
|
|
376
|
-
c.add(stack="java-servlet", kind="declared_route", method=["ANY"], path=pattern,
|
|
377
|
-
handler={"file": hfile, "line": 1, "symbol": cls or name}, id_params=[], guard_hints=[],
|
|
378
|
-
data_hints=[], notes=f"mapped in {rel}:{line_of(text, m.start())}")
|
|
379
|
-
found = True
|
|
380
|
-
if "struts-config" in rel:
|
|
381
|
-
for m in re.finditer(r"<action\b([^>]*)>", text):
|
|
382
|
-
attrs = dict(re.findall(r"(\w+)\s*=\s*\"([^\"]*)\"", m.group(1)))
|
|
383
|
-
if "path" not in attrs:
|
|
384
|
-
continue
|
|
385
|
-
cls = attrs.get("type", "")
|
|
386
|
-
hfile = class_index.get(cls.split(".")[-1], rel)
|
|
387
|
-
note = f"declared in {rel}:{line_of(text, m.start())}"
|
|
388
|
-
if attrs.get("parameter"):
|
|
389
|
-
note += f"; DispatchAction parameter '{attrs['parameter']}' -> one endpoint per method"
|
|
390
|
-
c.add(stack="java-struts1", kind="declared_route", method=["ANY"], path=attrs["path"] + ".do",
|
|
391
|
-
handler={"file": hfile, "line": 1, "symbol": cls}, id_params=[], guard_hints=[], data_hints=[], notes=note)
|
|
392
|
-
found = True
|
|
393
|
-
if rel.endswith("struts.xml"):
|
|
394
|
-
for pm in re.finditer(r"<package\b([^>]*)>([\s\S]*?)</package>", text):
|
|
395
|
-
pattrs = dict(re.findall(r"(\w+)\s*=\s*\"([^\"]*)\"", pm.group(1)))
|
|
396
|
-
ns = pattrs.get("namespace", "/").rstrip("/")
|
|
397
|
-
for am in re.finditer(r"<action\b([^>]*)>", pm.group(2)):
|
|
398
|
-
attrs = dict(re.findall(r"(\w+)\s*=\s*\"([^\"]*)\"", am.group(1)))
|
|
399
|
-
cls = attrs.get("class", "")
|
|
400
|
-
hfile = class_index.get(cls.split(".")[-1], rel)
|
|
401
|
-
c.add(stack="java-struts2", kind="declared_route", method=["ANY"], path=f"{ns}/{attrs.get('name', '?')}.action",
|
|
402
|
-
handler={"file": hfile, "line": 1, "symbol": f"{cls}#{attrs.get('method', 'execute')}"},
|
|
403
|
-
id_params=[], guard_hints=[], data_hints=[],
|
|
404
|
-
notes=f"declared in {rel}:{line_of(text, pm.start() + am.start())}; action setters are inputs")
|
|
405
|
-
found = True
|
|
406
|
-
return found
|
|
407
|
-
|
|
408
|
-
|
|
409
309
|
def dotnet_declared(c: Collector, rel: str, text: str) -> bool:
|
|
410
310
|
found = False
|
|
411
311
|
cm = re.search(r"(?:\[ApiController\][\s\S]{0,100}?)?class\s+(\w+?)Controller\s*(?::\s*(\w+))?|Class\s+(\w+?)Controller[\s\S]{0,80}?Inherits\s+(\w+)", text, re.I)
|
|
@@ -560,48 +460,6 @@ GO_FRAMEWORKS = [("go-gin", "github.com/gin-gonic/gin"), ("go-echo", "github.com
|
|
|
560
460
|
GO_PATTERN = re.compile(r"^(?:([A-Z]+)\s+)?(/\S*)$")
|
|
561
461
|
|
|
562
462
|
|
|
563
|
-
def call_args(text: str, open_idx: int, limit: int = 4000) -> Tuple[str, int]:
|
|
564
|
-
"""Return (argument text, index after the closing paren) for the call whose '(' is at open_idx."""
|
|
565
|
-
depth, i, end = 0, open_idx, min(len(text), open_idx + limit)
|
|
566
|
-
while i < end:
|
|
567
|
-
ch = text[i]
|
|
568
|
-
if ch in "\"`":
|
|
569
|
-
close = text.find(ch, i + 1)
|
|
570
|
-
while ch == '"' and close != -1 and text[close - 1] == "\\":
|
|
571
|
-
close = text.find(ch, close + 1)
|
|
572
|
-
i = close if close != -1 else end
|
|
573
|
-
elif ch in "([{":
|
|
574
|
-
depth += 1
|
|
575
|
-
elif ch in ")]}":
|
|
576
|
-
depth -= 1
|
|
577
|
-
if depth == 0:
|
|
578
|
-
return text[open_idx + 1:i], i + 1
|
|
579
|
-
i += 1
|
|
580
|
-
return text[open_idx + 1:end], end
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
def split_top(args: str) -> List[str]:
|
|
584
|
-
"""Split call arguments on top-level commas."""
|
|
585
|
-
parts, depth, cur, quote = [], 0, [], None
|
|
586
|
-
for ch in args:
|
|
587
|
-
if quote:
|
|
588
|
-
quote = None if ch == quote else quote
|
|
589
|
-
elif ch in "\"`":
|
|
590
|
-
quote = ch
|
|
591
|
-
elif ch in "([{":
|
|
592
|
-
depth += 1
|
|
593
|
-
elif ch in ")]}":
|
|
594
|
-
depth -= 1
|
|
595
|
-
elif ch == "," and depth == 0:
|
|
596
|
-
parts.append("".join(cur).strip())
|
|
597
|
-
cur = []
|
|
598
|
-
continue
|
|
599
|
-
cur.append(ch)
|
|
600
|
-
if "".join(cur).strip():
|
|
601
|
-
parts.append("".join(cur).strip())
|
|
602
|
-
return parts
|
|
603
|
-
|
|
604
|
-
|
|
605
463
|
def guards_before(c: Collector, text: str, start: int, end: int, span: int = 500) -> List[Dict]:
|
|
606
464
|
ws = max(0, start - span)
|
|
607
465
|
return c.guards(text[ws:end], line_of(text, ws))
|
|
@@ -712,6 +570,9 @@ def file_endpoint(c: Collector, rel: str, text: str, lang: str, ext: str, target
|
|
|
712
570
|
wr = wr.strip("/") + "/"
|
|
713
571
|
if rel.startswith(wr):
|
|
714
572
|
path = "/" + rel[len(wr):]
|
|
573
|
+
root = JAVA_WEB_ROOT.search(rel) if lang == "java" and path == "/" + rel else None
|
|
574
|
+
if root:
|
|
575
|
+
path = "/" + rel[root.end():]
|
|
715
576
|
stack = {"php": "php", "java": "java-jsp", "dotnet": "dotnet-webforms" if ext in (".aspx",) else "dotnet-handlers",
|
|
716
577
|
"asp": "classic-asp", "perl": "perl-cgi", "cfml": "coldfusion"}.get(lang, lang)
|
|
717
578
|
if ext in (".asmx", ".svc"):
|
|
@@ -775,7 +636,11 @@ def _scan_route_file_batch(args: Tuple[List[Tuple[str, str]], Dict, set, str]) -
|
|
|
775
636
|
declared = go_declared(c, rel, t)
|
|
776
637
|
if declared or ext not in FILE_ENDPOINT_EXTS:
|
|
777
638
|
continue
|
|
778
|
-
if
|
|
639
|
+
if ext == ".jsp":
|
|
640
|
+
# Tomcat serves any JSP outside WEB-INF, whatever its directory is called; those inside are only forwarded to
|
|
641
|
+
if "/WEB-INF/" in "/" + rel:
|
|
642
|
+
continue
|
|
643
|
+
elif INCLUDE_DIR_RE.search(rel.rsplit("/", 1)[0] + "/" if "/" in rel else ""):
|
|
779
644
|
continue
|
|
780
645
|
if framework_php and lang == "php" and re.search(r"(^|/)(application|system|app|vendor|routes|resources|storage|bootstrap)/", rel):
|
|
781
646
|
continue
|
|
@@ -791,15 +656,22 @@ def map_routes(target: Path, config: Dict, stack_ids: Optional[set] = None, work
|
|
|
791
656
|
files = list(iter_source_files(target, config))
|
|
792
657
|
stack_ids = stack_ids or set()
|
|
793
658
|
framework_php = bool(stack_ids & {"php-codeigniter", "php-cakephp", "php-laravel", "php-zend1"})
|
|
794
|
-
class_index =
|
|
659
|
+
class_index = build_class_index(rel for _, rel in files)
|
|
795
660
|
texts: Dict[str, str] = {}
|
|
796
661
|
|
|
797
|
-
# 1. XML pre-pass (web.xml /
|
|
798
|
-
for p, rel in files
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
662
|
+
# 1. XML pre-pass (web.xml / Struts / Spring declarative mappings)
|
|
663
|
+
xml_texts = {rel: read_text(p, enc) for p, rel in files if p.suffix.lower() == ".xml"}
|
|
664
|
+
modules: Dict[str, str] = {}
|
|
665
|
+
beans: Dict[str, str] = {}
|
|
666
|
+
for rel, t in xml_texts.items():
|
|
667
|
+
if rel.endswith("web.xml"):
|
|
668
|
+
modules.update(struts_modules(rel, t))
|
|
669
|
+
if "<bean" in t:
|
|
670
|
+
beans.update(collect_spring_beans(t))
|
|
671
|
+
extension = struts2_action_extension(target, config, xml_texts.values(), enc)
|
|
672
|
+
for rel, t in xml_texts.items():
|
|
673
|
+
if "<servlet" in t or "<action" in t or "UrlHandlerMapping" in t:
|
|
674
|
+
xml_declared(c, rel, t, class_index, modules, beans, extension)
|
|
803
675
|
|
|
804
676
|
xml_items = list(c.items)
|
|
805
677
|
|
|
@@ -865,20 +737,24 @@ def map_routes(target: Path, config: Dict, stack_ids: Optional[set] = None, work
|
|
|
865
737
|
# post-pass 1: routes declared in XML -> enrich from the handler class file
|
|
866
738
|
for it in c.items:
|
|
867
739
|
hf = it["handler"]["file"]
|
|
740
|
+
if it["kind"] == "declared_route" and it["handler"]["line"] == 1 and hf in texts and hf.endswith(".java"):
|
|
741
|
+
it["handler"]["line"] = entry_method_line(texts[hf], it["handler"].get("symbol")) or 1
|
|
868
742
|
if it["kind"] == "declared_route" and not it["id_params"] and hf in texts and hf.endswith(".java"):
|
|
869
743
|
t = texts[hf]
|
|
870
744
|
it["id_params"] = c.inputs("java", t)
|
|
871
745
|
it["guard_hints"] = it["guard_hints"] or c.guards(t)
|
|
872
746
|
it["data_hints"] = it["data_hints"] or c.data_ops(t)
|
|
873
747
|
# post-pass 2: servlet classes without @WebServlet -> take the path from web.xml
|
|
874
|
-
xml_paths
|
|
875
|
-
|
|
748
|
+
xml_paths: Dict[str, List[str]] = {}
|
|
749
|
+
for it in c.items:
|
|
750
|
+
if it["stack"] == "java-servlet" and it["notes"].startswith("mapped in "):
|
|
751
|
+
xml_paths.setdefault(it["handler"]["file"], []).append(it["path"])
|
|
876
752
|
keep = []
|
|
877
753
|
for it in c.items:
|
|
878
754
|
if it["path"] == "<see web.xml servlet-mapping>" and it["handler"]["file"] in xml_paths:
|
|
879
|
-
it
|
|
880
|
-
|
|
881
|
-
|
|
755
|
+
keep.extend(dict(it, path=mapped, notes="path from web.xml") for mapped in xml_paths[it["handler"]["file"]])
|
|
756
|
+
else:
|
|
757
|
+
keep.append(it)
|
|
882
758
|
mapped_with_methods = {it["handler"]["file"] for it in keep if it["notes"] == "path from web.xml"}
|
|
883
759
|
keep = [it for it in keep if not (it["notes"].startswith("mapped in ") and it["handler"]["file"] in mapped_with_methods)]
|
|
884
760
|
for n, it in enumerate(keep, 1):
|
|
@@ -403,6 +403,8 @@ def main() -> int:
|
|
|
403
403
|
"injection_candidates_without_verdict", "bfla_routes_without_verdict",
|
|
404
404
|
"mass_candidates_without_verdict", "validation_errors",
|
|
405
405
|
"new_issues_from_verifier"))
|
|
406
|
+
for error in cov.get("validation_errors") or []:
|
|
407
|
+
print(f"validation error (finding not merged): {error}", file=sys.stderr)
|
|
406
408
|
print(json.dumps({"summary": summary, "coverage_gaps": gaps}, indent=2))
|
|
407
409
|
return 1 if gaps else 0
|
|
408
410
|
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
"""Secret redaction for code excerpts: Tier 1 snippets and every excerpt the headless agents send to a model (#350).
|
|
2
|
+
|
|
3
|
+
Pattern-based, so it cannot catch every secret: the pilot runbook also leaves key and environment config files
|
|
4
|
+
out of the source copy. Standard-library only (Python 3.8+ compatible).
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import re
|
|
10
|
+
from typing import List, Optional, Tuple
|
|
11
|
+
|
|
12
|
+
MASK = "[REDACTED]"
|
|
13
|
+
# Longer lines (minified bundles, generated data) are dropped whole: they are not useful to an auditor and
|
|
14
|
+
# keep the regexes below from running on megabyte-long input
|
|
15
|
+
MAX_LINE_CHARS = 4000
|
|
16
|
+
|
|
17
|
+
# A secret-like key name; the lookahead keeps passwordPolicy, tokenizer and Passenger out
|
|
18
|
+
_KW = r"(?:password|passwd|passphrase|pwd|pw|pass|secret|credential|api[_-]?key|token)(?![a-z])"
|
|
19
|
+
_NAME_TAGS = r"(?:param-name|name|key)"
|
|
20
|
+
_VALUE_TAGS = r"(?:param-value|value)"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _rx(pattern: str, flags: int = re.IGNORECASE) -> re.Pattern[str]:
|
|
24
|
+
return re.compile(pattern, flags)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
# (pattern, replacement); applied in order to each line
|
|
28
|
+
_RULES: Tuple[Tuple[re.Pattern[str], str], ...] = (
|
|
29
|
+
# -----BEGIN PRIVATE KEY----- ... -----END PRIVATE KEY----- on one line
|
|
30
|
+
(_rx(r"(-----BEGIN [A-Z ]*PRIVATE KEY-----).*?(-----END [A-Z ]*PRIVATE KEY-----)", 0), r"\1" + MASK + r"\2"),
|
|
31
|
+
# "password": "x", 'password' => 'x', pw = "x", keystorePass="x", setProperty("mail.smtp.password", "x")
|
|
32
|
+
(_rx(r"(?<![\w.\-])((['\"]?)[\w.\-]*?" + _KW + r"\2\s*(?:=>|->|[=:,])\s*)(['\"])[^'\"]+\3"), r"\1\3" + MASK + r"\3"),
|
|
33
|
+
# conn.setPassword("x"), setKeystorePass("x")
|
|
34
|
+
(_rx(r"(\bset\w*?" + _KW + r"\s*\(\s*)(['\"])[^'\"]*\2"), r"\1\2" + MASK + r"\2"),
|
|
35
|
+
# DriverManager.getConnection(url, "sa", "x")
|
|
36
|
+
(_rx(r"(\bgetConnection\s*\([^,()]*,[^,()]*,\s*)(['\"])[^'\"]*\2"), r"\1\2" + MASK + r"\2"),
|
|
37
|
+
# new PasswordAuthentication("u", "x".toCharArray())
|
|
38
|
+
(_rx(r"(\bPasswordAuthentication\s*\([^,()]*,\s*)(['\"])[^'\"]*\2"), r"\1\2" + MASK + r"\2"),
|
|
39
|
+
# <property name="jdbc.password" value="x"/>
|
|
40
|
+
(_rx(r"(\bname\s*=\s*['\"][^'\"]*?" + _KW + r"[^'\"]*['\"][^>]*?\bvalue\s*=\s*)(['\"])[^'\"]*\2"),
|
|
41
|
+
r"\1\2" + MASK + r"\2"),
|
|
42
|
+
# <param-name>password</param-name><param-value>x</param-value>
|
|
43
|
+
(_rx(r"(<" + _NAME_TAGS + r">[^<]*?" + _KW + r"[^<]*</" + _NAME_TAGS + r">\s*<" + _VALUE_TAGS + r">)[^<]+"),
|
|
44
|
+
r"\1" + MASK),
|
|
45
|
+
# <password>x</password>
|
|
46
|
+
(_rx(r"(<([\w.\-:]*?" + _KW + r")\s*>)[^<]+(?=</\2\s*>)"), r"\1" + MASK),
|
|
47
|
+
# jdbc:sqlserver://h;password=x ?pwd=x& password={a;b}
|
|
48
|
+
(_rx(r"([;?&](?:password|passwd|pwd)=)(?:\{[^}]*\}|[^;\"'&\s]+)"), r"\1" + MASK),
|
|
49
|
+
# scheme://user:pass@host
|
|
50
|
+
(_rx(r"(?<![a-z0-9+.\-])([a-z][a-z0-9+.\-]*://[^/\s:@\"']+:)[^@\s\"'/]+@"), r"\1" + MASK + "@"),
|
|
51
|
+
# jdbc:oracle:thin:user/pass@host
|
|
52
|
+
(_rx(r"(\bjdbc:oracle:\w+:[^/\s:@\"']+/)[^@\s\"']+@"), r"\1" + MASK + "@"),
|
|
53
|
+
# Authorization: Basic xxx / Bearer xxx
|
|
54
|
+
(_rx(r"(\b(?:Basic|Bearer)\s+)[A-Za-z0-9._~+/=\-]{8,}"), r"\1" + MASK),
|
|
55
|
+
# Well-known token formats (case-sensitive)
|
|
56
|
+
(_rx(r"\b(?:AKIA[0-9A-Z]{16}|gh[pousr]_[A-Za-z0-9]{30,}|github_pat_\w{20,}|sk-[A-Za-z0-9_\-]{20,}"
|
|
57
|
+
r"|AIza[0-9A-Za-z_\-]{35})\b", 0), MASK),
|
|
58
|
+
# Unquoted config values (.properties, YAML, .env) on their own line; ${PLACEHOLDER} and code are kept
|
|
59
|
+
(_rx(r"^(\s*[\w.\-]*?" + _KW + r"[\w.\-]*\s*[=:]\s*)(?![\"'$=\s])[^\s\"'(){}<>,;]+\s*(?:(?:#|//).*)?$"), r"\1" + MASK),
|
|
60
|
+
# Dotted config keys inside comments or strings: <%-- db.password=x --%>
|
|
61
|
+
(_rx(r"(?<![\w.\-])([\w\-]+\.[\w.\-]*?" + _KW + r"[\w\-]*\s*=\s*)(?![\"'$=\s])[^\s\"'(){}<>,;]+(?=\s|$|[\"'])"),
|
|
62
|
+
r"\1" + MASK),
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
_PRIVATE_KEY_BEGIN = _rx(r"-----BEGIN [A-Z ]*PRIVATE KEY-----", 0)
|
|
66
|
+
_PRIVATE_KEY_END = _rx(r"-----END [A-Z ]*PRIVATE KEY-----", 0)
|
|
67
|
+
# A secret name whose value follows on a later line: <property name="password"\n value="x"/>, web.xml params
|
|
68
|
+
_PENDING_NAME = _rx(r"\bname\s*=\s*['\"][^'\"]*?" + _KW + r"[^'\"]*['\"]|<" + _NAME_TAGS + r">[^<]*?" + _KW)
|
|
69
|
+
_PENDING_VALUE: Tuple[Tuple[re.Pattern[str], str], ...] = (
|
|
70
|
+
(_rx(r"(\bvalue\s*=\s*)(['\"])[^'\"]*\2"), r"\1\2" + MASK + r"\2"),
|
|
71
|
+
(_rx(r"(<" + _VALUE_TAGS + r">)[^<]+"), r"\1" + MASK),
|
|
72
|
+
)
|
|
73
|
+
_PENDING_LINES = 2
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _too_long(s: str) -> Optional[str]:
|
|
77
|
+
return f"[line of {len(s)} characters omitted]" if len(s) > MAX_LINE_CHARS else None
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def redact(s: str) -> str:
|
|
81
|
+
"""Mask secrets in one line of code or config."""
|
|
82
|
+
omitted = _too_long(s)
|
|
83
|
+
if omitted:
|
|
84
|
+
return omitted
|
|
85
|
+
for pattern, repl in _RULES:
|
|
86
|
+
s = pattern.sub(repl, s)
|
|
87
|
+
return s
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _redact_pending(line: str) -> Tuple[str, bool]:
|
|
91
|
+
for pattern, repl in _PENDING_VALUE:
|
|
92
|
+
masked, n = pattern.subn(repl, line)
|
|
93
|
+
if n:
|
|
94
|
+
return masked, True
|
|
95
|
+
return line, False
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def redact_lines(lines: List[str]) -> List[str]:
|
|
99
|
+
"""Redact each line, blank out private key bodies and mask values split from their names.
|
|
100
|
+
|
|
101
|
+
The line count stays the same, so line numbers in excerpts remain valid.
|
|
102
|
+
"""
|
|
103
|
+
out: List[str] = []
|
|
104
|
+
in_key, pending = False, 0
|
|
105
|
+
for line in lines:
|
|
106
|
+
if in_key:
|
|
107
|
+
in_key = not _PRIVATE_KEY_END.search(line)
|
|
108
|
+
out.append(MASK if in_key else line)
|
|
109
|
+
continue
|
|
110
|
+
masked = redact(line)
|
|
111
|
+
if pending:
|
|
112
|
+
masked, done = _redact_pending(masked)
|
|
113
|
+
pending = 0 if done else pending - 1
|
|
114
|
+
if _PENDING_NAME.search(line) and MASK not in masked:
|
|
115
|
+
pending = _PENDING_LINES
|
|
116
|
+
in_key = bool(_PRIVATE_KEY_BEGIN.search(line)) and not _PRIVATE_KEY_END.search(line)
|
|
117
|
+
out.append(masked)
|
|
118
|
+
return out
|