@hoanghainh/komagu 2.4.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.
Files changed (55) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/LICENSE +202 -0
  3. package/README.md +1 -0
  4. package/assess/lsa/scripts/detect_stack.py +13 -5
  5. package/assess/lsa/scripts/find_injection.py +35 -2
  6. package/assess/lsa/scripts/java_routes.py +323 -0
  7. package/assess/lsa/scripts/lsa_common.py +72 -2
  8. package/assess/lsa/scripts/map_routes.py +36 -160
  9. package/assess/lsa/scripts/merge_findings.py +2 -0
  10. package/assess/lsa/tests/test_java_legacy_realistic.py +453 -0
  11. package/contracts/schemas/persona-suite.schema.json +38 -2
  12. package/examples/member-portal/backend-dotnet/Dockerfile +2 -1
  13. package/examples/member-portal/backend-spring/Dockerfile +2 -0
  14. package/examples/member-portal/docker-compose.yml +1 -1
  15. package/examples/member-portal/frontend/Dockerfile +3 -2
  16. package/examples/member-portal/frontend/package-lock.json +24 -656
  17. package/examples/member-portal/frontend/package.json +1 -1
  18. package/operate/ai/runner.py +20 -2
  19. package/operate/cli/__init__.py +1 -1
  20. package/operate/cli/commands/fix.py +4 -0
  21. package/operate/cli/commands/leak_check.py +75 -0
  22. package/operate/cli/commands/protect.py +32 -3
  23. package/operate/cli/commands/test.py +131 -15
  24. package/operate/cli/main.py +5 -3
  25. package/operate/cross_user_tester/persona_matrix.py +118 -25
  26. package/operate/cross_user_tester/runner.py +190 -7
  27. package/operate/cross_user_tester/session_config.py +180 -0
  28. package/operate/cross_user_tester/session_http.py +288 -0
  29. package/operate/devsecops/leak_check.py +314 -0
  30. package/operate/remediator/runner.py +18 -1
  31. package/operate/remediator/strategies.py +2 -2
  32. package/operate/simulate/scenarios.py +2 -2
  33. package/operate/tests/mock_legacy_session_app.py +160 -0
  34. package/operate/tests/test_cross_user_session.py +565 -0
  35. package/operate/tests/test_docker_packaging.py +3 -3
  36. package/operate/tests/test_headless_ai.py +6 -0
  37. package/operate/tests/test_leak_check.py +250 -0
  38. package/operate/tests/test_legacy_session_edge.py +192 -0
  39. package/operate/tests/test_pilot_safeguards.py +245 -0
  40. package/operate/tests/test_remediator_java_legacy.py +330 -0
  41. package/package.json +2 -1
  42. package/remediate/authz/patcher.py +120 -11
  43. package/remediate/authz/tests/test_bola_patcher.py +4 -1
  44. package/remediate/edge/coraza/Caddyfile +1 -1
  45. package/remediate/edge/coraza/Dockerfile +9 -0
  46. package/remediate/edge/coraza/rules/coraza-crs-custom.conf +12 -0
  47. package/remediate/edge/coraza/virtual_patcher.py +24 -0
  48. package/remediate/edge/docker-compose.yml +7 -2
  49. package/remediate/edge/kong/kong-legacy-session.yaml +51 -0
  50. package/remediate/edge/mock-backend/Dockerfile +2 -0
  51. package/remediate/edge/tests/test_edge_security.py +21 -1
  52. package/remediate/resilience/terraform/backup_worm.tf +39 -1
  53. package/remediate/resilience/terraform/main.tf +15 -3
  54. package/remediate/resilience/terraform/variables.tf +11 -0
  55. package/remediate/resilience/tests/test_resilience.py +16 -0
@@ -379,7 +379,7 @@ class IDCFrontierRansomwareScenario(BaseScenario):
379
379
  self.target_dir.mkdir(parents=True, exist_ok=True)
380
380
  test_file.write_text("Komagu Immutable WORM Backup - Read Only", encoding="utf-8")
381
381
  # Mark file read-only (simulating local WORM compliance mode)
382
- os.chmod(str(test_file), 0o444)
382
+ os.chmod(str(test_file), 0o400)
383
383
  except Exception:
384
384
  pass
385
385
 
@@ -416,7 +416,7 @@ class IDCFrontierRansomwareScenario(BaseScenario):
416
416
 
417
417
  # Clean up
418
418
  try:
419
- os.chmod(str(test_file), 0o666)
419
+ os.chmod(str(test_file), 0o600)
420
420
  test_file.unlink(missing_ok=True)
421
421
  except Exception:
422
422
  pass
@@ -0,0 +1,160 @@
1
+ #!/usr/bin/env python3
2
+ """Small session-based app that behaves like a JSP / Struts application.
3
+
4
+ Used to rehearse ``komagu test`` on legacy behaviour:
5
+
6
+ - login state lives in a ``JSESSIONID`` cookie;
7
+ - writes are ``application/x-www-form-urlencoded`` and need a single-use Struts token;
8
+ - a successful write answers 302 (post/redirect/get);
9
+ - a denied request is a 200 Shift_JIS error page, a 302 to login, or a 404,
10
+ depending on ``server.denial``;
11
+ - ``server.fixed`` is the ownership check being deployed.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import secrets
17
+ import threading
18
+ from http.server import BaseHTTPRequestHandler, HTTPServer
19
+ from typing import Dict, List, NamedTuple, Optional, Tuple
20
+ from urllib.parse import parse_qs, urlsplit
21
+
22
+ ALICE_COOKIE = "JSESSIONID=sess-alice-0001"
23
+ BOB_COOKIE = "JSESSIONID=sess-bob-0002"
24
+ EXPIRED_COOKIE = "JSESSIONID=sess-expired-0003"
25
+ TOKEN_FIELD = "org.apache.struts.taglib.html.TOKEN"
26
+ DENIED_TEXT = "アクセス権限がありません"
27
+ LOGIN_REDIRECT = "/login.do;jsessionid=sess-rewritten-url-9999"
28
+
29
+
30
+ class Seen(NamedTuple):
31
+ method: str
32
+ path: str
33
+ cookie: Optional[str]
34
+ authorization: Optional[str]
35
+ content_type: Optional[str]
36
+ body: str
37
+
38
+
39
+ class LegacyHandler(BaseHTTPRequestHandler):
40
+ server: "LegacyServer"
41
+
42
+ def log_message(self, format: str, *args: object) -> None: # noqa: A002
43
+ pass
44
+
45
+ def _user(self) -> Optional[str]:
46
+ cookie = self.headers.get("Cookie", "")
47
+ pairs = dict(p.strip().split("=", 1) for p in cookie.split(";") if "=" in p)
48
+ return self.server.sessions.get(pairs.get("JSESSIONID", ""))
49
+
50
+ def _send(self, status: int, body: bytes = b"", charset: str = "UTF-8", location: Optional[str] = None) -> None:
51
+ self.send_response(status)
52
+ self.send_header("Content-Type", f"text/html; charset={charset}")
53
+ self.send_header("Content-Length", str(len(body)))
54
+ if location:
55
+ self.send_header("Location", location)
56
+ self.end_headers()
57
+ self.wfile.write(body)
58
+
59
+ def _to_login(self) -> None:
60
+ self._send(302, location=LOGIN_REDIRECT)
61
+
62
+ def _deny(self) -> None:
63
+ if self.server.denial == "login_redirect":
64
+ self._to_login()
65
+ elif self.server.denial == "not_found":
66
+ self._send(404, b"<html>Not Found</html>")
67
+ else:
68
+ page = f"<html><body><p>{DENIED_TEXT}</p></body></html>"
69
+ self._send(200, page.encode("cp932"), charset="Windows-31J")
70
+
71
+ def _may_access(self, user: str, order_id: str) -> bool:
72
+ owner = self.server.orders.get(order_id)
73
+ if owner is None:
74
+ return False
75
+ return owner == user or not self.server.fixed
76
+
77
+ def _record(self, method: str, body: str = "") -> None:
78
+ self.server.seen.append(Seen(
79
+ method, self.path, self.headers.get("Cookie"), self.headers.get("Authorization"),
80
+ self.headers.get("Content-Type"), body,
81
+ ))
82
+
83
+ def do_GET(self) -> None: # noqa: N802
84
+ self._record("GET")
85
+ url = urlsplit(self.path)
86
+ query = parse_qs(url.query)
87
+ if url.path == "/login.do":
88
+ self._send(200, b"<html>Please log in</html>")
89
+ return
90
+ user = self._user()
91
+ if user is None:
92
+ self._to_login()
93
+ elif url.path == "/home.do":
94
+ self._send(200, f"<html>Welcome {user}</html>".encode())
95
+ elif url.path == "/order/view.do":
96
+ self._view(user, query.get("id", [""])[0])
97
+ elif url.path == "/order/edit.do":
98
+ self._edit_form(user)
99
+ else:
100
+ self._send(404, b"<html>No such action</html>")
101
+
102
+ def _view(self, user: str, order_id: str) -> None:
103
+ if not self._may_access(user, order_id):
104
+ self._deny()
105
+ return
106
+ self._send(200, f"<html>Order detail {order_id}: secret-note</html>".encode())
107
+
108
+ def _edit_form(self, user: str) -> None:
109
+ if self.server.hide_token:
110
+ self._send(200, b"<html><form></form></html>")
111
+ return
112
+ token = secrets.token_hex(8)
113
+ self.server.tokens[user] = token
114
+ # value before name, as some tag libraries render it.
115
+ page = f'<html><form><input value="{token}" type="hidden" name="{TOKEN_FIELD}"></form></html>'
116
+ self._send(200, page.encode())
117
+
118
+ def do_POST(self) -> None: # noqa: N802
119
+ length = int(self.headers.get("Content-Length") or 0)
120
+ raw = self.rfile.read(length).decode("utf-8", "replace")
121
+ self._record("POST", raw)
122
+ user = self._user()
123
+ if user is None:
124
+ self._to_login()
125
+ return
126
+ if self.headers.get("Content-Type", "").split(";")[0] != "application/x-www-form-urlencoded":
127
+ self._send(415, b"<html>Form post expected</html>")
128
+ return
129
+ form = parse_qs(raw)
130
+ if urlsplit(self.path).path != "/order/update.do":
131
+ self._send(404, b"<html>No such action</html>")
132
+ return
133
+ if form.get(TOKEN_FIELD, [""])[0] != self.server.tokens.pop(user, None):
134
+ self._send(400, b"<html>Invalid token</html>")
135
+ return
136
+ order_id = form.get("id", [""])[0]
137
+ if not self._may_access(user, order_id):
138
+ self._deny()
139
+ return
140
+ self.server.updates.append((user, order_id))
141
+ self._send(302, location="/order/list.do")
142
+
143
+
144
+ class LegacyServer(HTTPServer):
145
+ def __init__(self) -> None:
146
+ super().__init__(("127.0.0.1", 0), LegacyHandler)
147
+ self.fixed = False
148
+ self.denial = "error_page"
149
+ self.hide_token = False
150
+ self.orders: Dict[str, str] = {"100": "alice", "200": "bob"}
151
+ self.sessions: Dict[str, str] = {"sess-alice-0001": "alice", "sess-bob-0002": "bob"}
152
+ self.tokens: Dict[str, str] = {}
153
+ self.updates: List[Tuple[str, str]] = []
154
+ self.seen: List[Seen] = []
155
+
156
+
157
+ def start_legacy_app() -> Tuple[LegacyServer, str]:
158
+ server = LegacyServer()
159
+ threading.Thread(target=server.serve_forever, daemon=True).start()
160
+ return server, f"http://127.0.0.1:{server.server_port}"