@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.
- package/CHANGELOG.md +24 -0
- package/LICENSE +202 -0
- package/README.md +1 -0
- package/assess/lsa/scripts/detect_stack.py +13 -5
- 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 +72 -2
- package/assess/lsa/scripts/map_routes.py +36 -160
- package/assess/lsa/scripts/merge_findings.py +2 -0
- package/assess/lsa/tests/test_java_legacy_realistic.py +453 -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/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_cross_user_session.py +565 -0
- package/operate/tests/test_docker_packaging.py +3 -3
- package/operate/tests/test_headless_ai.py +6 -0
- 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
|
@@ -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),
|
|
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),
|
|
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}"
|