@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
|
@@ -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}"
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Secrets are masked in every code snippet the headless agents send to the model (#350)."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import shutil
|
|
7
|
+
import tempfile
|
|
8
|
+
import unittest
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
import time
|
|
12
|
+
|
|
13
|
+
from operate.ai.context import _model_source, _read_snippet, _redact_lines, _verification_context
|
|
14
|
+
|
|
15
|
+
JAVA_DAO = '''package com.acme.dao;
|
|
16
|
+
public class AccountDao {
|
|
17
|
+
private static final String URL = "jdbc:sqlserver://db01;databaseName=ams;user=sa;password=Ams#2019!";
|
|
18
|
+
private static final String MONGO = "mongodb://svc:Mongo2020pw@mongo01:27017/ams";
|
|
19
|
+
private static final String ORA = "jdbc:oracle:thin:scott/Tiger2018@db02:1521/AMS";
|
|
20
|
+
// db.password=CommentPw7 (old config, kept for reference)
|
|
21
|
+
private String apiKey = "sk-live-abcdef123456";
|
|
22
|
+
public Account find(HttpServletRequest req) {
|
|
23
|
+
String password = req.getParameter("password");
|
|
24
|
+
String sql = "SELECT * FROM account WHERE id = " + req.getParameter("id");
|
|
25
|
+
return jdbc.query(sql);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
'''
|
|
29
|
+
|
|
30
|
+
PROPERTIES = '''spring.datasource.url=jdbc:oracle:thin:@db02:1521/AMS
|
|
31
|
+
spring.datasource.username=ams_app
|
|
32
|
+
spring.datasource.password=Oracle#Pw2021
|
|
33
|
+
mail.smtp.password: smtpPw99
|
|
34
|
+
app.jwt.secret=c2VjcmV0LWtleS1mb3ItYW1z
|
|
35
|
+
app.token.placeholder=${APP_TOKEN}
|
|
36
|
+
'''
|
|
37
|
+
|
|
38
|
+
PRIVATE_KEY = '''-----BEGIN RSA PRIVATE KEY-----
|
|
39
|
+
MIIEowIBAAKCAQEAwJ1rdTw6kLqA
|
|
40
|
+
Zm9vYmFyYmF6cXV4cXV1eHF1dXg=
|
|
41
|
+
-----END RSA PRIVATE KEY-----
|
|
42
|
+
class Signer {}
|
|
43
|
+
'''
|
|
44
|
+
|
|
45
|
+
JSP = '''<%-- legacy: ldap.bind.password=LdapPw55 --%>
|
|
46
|
+
<%@ page import="java.sql.*" %>
|
|
47
|
+
<% String id = request.getParameter("id"); %>
|
|
48
|
+
'''
|
|
49
|
+
|
|
50
|
+
SECRETS = ("Ams#2019!", "Mongo2020pw", "Tiger2018", "CommentPw7", "sk-live-abcdef123456", "Oracle#Pw2021", "smtpPw99",
|
|
51
|
+
"c2VjcmV0LWtleS1mb3ItYW1z", "MIIEowIBAAKCAQEAwJ1rdTw6kLqA", "Zm9vYmFyYmF6cXV4cXV1eHF1dXg=", "LdapPw55")
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class TestSnippetRedaction(unittest.TestCase):
|
|
55
|
+
def setUp(self) -> None:
|
|
56
|
+
self.target = Path(tempfile.mkdtemp(prefix="komagu_redact_"))
|
|
57
|
+
files = {"src/com/acme/dao/AccountDao.java": JAVA_DAO, "src/main/resources/application.properties": PROPERTIES,
|
|
58
|
+
"src/com/acme/Signer.java": PRIVATE_KEY, "WebContent/user/detail.jsp": JSP}
|
|
59
|
+
for rel, text in files.items():
|
|
60
|
+
(self.target / rel).parent.mkdir(parents=True, exist_ok=True)
|
|
61
|
+
(self.target / rel).write_text(text, encoding="utf-8")
|
|
62
|
+
|
|
63
|
+
def tearDown(self) -> None:
|
|
64
|
+
shutil.rmtree(self.target, ignore_errors=True)
|
|
65
|
+
|
|
66
|
+
def assert_no_secret(self, text: str) -> None:
|
|
67
|
+
for secret in SECRETS:
|
|
68
|
+
self.assertNotIn(secret, text)
|
|
69
|
+
|
|
70
|
+
def test_java_source_snippet_masks_connection_strings_and_keys(self) -> None:
|
|
71
|
+
snip = _read_snippet(self.target, "src/com/acme/dao/AccountDao.java", 5)
|
|
72
|
+
self.assert_no_secret(snip)
|
|
73
|
+
self.assertIn("[REDACTED]", snip)
|
|
74
|
+
|
|
75
|
+
def test_code_the_auditor_needs_is_kept(self) -> None:
|
|
76
|
+
snip = _read_snippet(self.target, "src/com/acme/dao/AccountDao.java", 5)
|
|
77
|
+
self.assertIn('String password = req.getParameter("password");', snip)
|
|
78
|
+
self.assertIn('"SELECT * FROM account WHERE id = " + req.getParameter("id")', snip)
|
|
79
|
+
self.assertIn("jdbc:sqlserver://db01;databaseName=ams;user=sa;password=[REDACTED]", snip)
|
|
80
|
+
self.assertIn("mongodb://svc:[REDACTED]@mongo01:27017/ams", snip)
|
|
81
|
+
self.assertIn("jdbc:oracle:thin:scott/[REDACTED]@db02:1521/AMS", snip)
|
|
82
|
+
|
|
83
|
+
def test_config_line_inside_a_comment_is_masked(self) -> None:
|
|
84
|
+
snip = _read_snippet(self.target, "WebContent/user/detail.jsp", 2)
|
|
85
|
+
self.assert_no_secret(snip)
|
|
86
|
+
self.assertIn("ldap.bind.password=[REDACTED] --%>", snip)
|
|
87
|
+
self.assertIn('String id = request.getParameter("id");', snip)
|
|
88
|
+
|
|
89
|
+
def test_assignments_in_code_are_kept(self) -> None:
|
|
90
|
+
lines = ["user.password = req.body.password;", "if (token == null) return;", "token: req.headers.authorization,"]
|
|
91
|
+
(self.target / "app.js").write_text("\n".join(lines) + "\n", encoding="utf-8")
|
|
92
|
+
snip = _read_snippet(self.target, "app.js", 2)
|
|
93
|
+
for line in lines:
|
|
94
|
+
self.assertIn(line, snip)
|
|
95
|
+
|
|
96
|
+
def test_unquoted_properties_values_are_masked(self) -> None:
|
|
97
|
+
snip = _read_snippet(self.target, "src/main/resources/application.properties", 3)
|
|
98
|
+
self.assert_no_secret(snip)
|
|
99
|
+
self.assertIn("spring.datasource.username=ams_app", snip)
|
|
100
|
+
self.assertIn("app.token.placeholder=${APP_TOKEN}", snip)
|
|
101
|
+
|
|
102
|
+
def test_private_key_block_is_masked_with_line_numbers_kept(self) -> None:
|
|
103
|
+
snip = _read_snippet(self.target, "src/com/acme/Signer.java", 3)
|
|
104
|
+
self.assert_no_secret(snip)
|
|
105
|
+
self.assertIn(" 5 | class Signer {}", snip)
|
|
106
|
+
|
|
107
|
+
def test_model_and_verifier_context_are_masked(self) -> None:
|
|
108
|
+
model = _model_source(self.target, "AccountDao", "src/com/acme/dao/AccountDao.java", 1)
|
|
109
|
+
self.assert_no_secret(model["code"])
|
|
110
|
+
ctx = _verification_context(self.target, {"location": {"file": "src/com/acme/dao/AccountDao.java", "start_line": 8},
|
|
111
|
+
"evidence": [{"file": "src/main/resources/application.properties", "line": 3}]})
|
|
112
|
+
self.assert_no_secret(str(ctx))
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
class TestJavaLegacySecretForms(unittest.TestCase):
|
|
116
|
+
"""Forms found in a security review of the first version (#350); each must hide Hunter2x."""
|
|
117
|
+
|
|
118
|
+
LEAKS = (
|
|
119
|
+
'<property name="password" value="Hunter2x"/>',
|
|
120
|
+
'<property name="jdbc.password"\n value="Hunter2x"/>',
|
|
121
|
+
"<param-name>db.password</param-name><param-value>Hunter2x</param-value>",
|
|
122
|
+
"<init-param>\n <param-name>password</param-name>\n <param-value>Hunter2x</param-value>",
|
|
123
|
+
"<password>Hunter2x</password>",
|
|
124
|
+
'conn.setPassword("Hunter2x");',
|
|
125
|
+
'props.setProperty("mail.smtp.password", "Hunter2x");',
|
|
126
|
+
'Connection c = DriverManager.getConnection(url, "sa", "Hunter2x");',
|
|
127
|
+
'return new PasswordAuthentication("svc", "Hunter2x".toCharArray());',
|
|
128
|
+
'String pw = "Hunter2x";',
|
|
129
|
+
'static final String DB_PASS = "Hunter2x";',
|
|
130
|
+
'<Connector port="8443" keystorePass="Hunter2x" truststorePass="Hunter2x"/>',
|
|
131
|
+
'{"password":"Hunter2x", "apikey" : "Hunter2x"}',
|
|
132
|
+
"$conf = array('password' => 'Hunter2x');",
|
|
133
|
+
"db_password: Hunter2x # prod",
|
|
134
|
+
"mail.smtp.pass=Hunter2x",
|
|
135
|
+
"ssl.key.passphrase=Hunter2x",
|
|
136
|
+
"jdbc:sqlserver://db;user=sa;password={Hunter2x;x}",
|
|
137
|
+
'conn.setRequestProperty("Authorization", "Basic SHVudGVyMng6eHl6");',
|
|
138
|
+
'h.put("Authorization", "Bearer Hunter2xHunter2x.eyJzdWIiOiIxIn0.sig");',
|
|
139
|
+
"-----BEGIN PRIVATE KEY----- Hunter2xMIIEvQIBADANBgkq -----END PRIVATE KEY-----",
|
|
140
|
+
)
|
|
141
|
+
TOKENS = ("AKIAIOSFODNN7EXAMPLE", "ghp_" + "a" * 36, "sk-ant-api03-" + "b" * 40, "AIza" + "c" * 35)
|
|
142
|
+
KEPT = (
|
|
143
|
+
'String password = req.getParameter("password");',
|
|
144
|
+
"String sql = \"SELECT * FROM users WHERE login='\" + u + \"' AND pwd='\" + p + \"'\";",
|
|
145
|
+
"<select id=\"login\">SELECT * FROM users WHERE pwd = #{password}</select>",
|
|
146
|
+
"spring.datasource.password=${DB_PASS}",
|
|
147
|
+
"user.password = req.body.password;",
|
|
148
|
+
"if (token == null) return;",
|
|
149
|
+
'String header = "Bearer " + token;',
|
|
150
|
+
'<property name="username" value="ams_app"/>',
|
|
151
|
+
"Passenger p = passengerDao.find(id);",
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
def redact(self, text: str) -> str:
|
|
155
|
+
return "\n".join(_redact_lines(text.split("\n")))
|
|
156
|
+
|
|
157
|
+
def test_java_legacy_forms_are_masked(self) -> None:
|
|
158
|
+
for sample in self.LEAKS:
|
|
159
|
+
with self.subTest(sample=sample):
|
|
160
|
+
self.assertNotIn("Hunter2x", self.redact(sample))
|
|
161
|
+
|
|
162
|
+
def test_cloud_and_vcs_tokens_are_masked(self) -> None:
|
|
163
|
+
for token in self.TOKENS:
|
|
164
|
+
with self.subTest(token=token[:8]):
|
|
165
|
+
self.assertNotIn(token, self.redact(f'String key = cfg.get("{token}");'))
|
|
166
|
+
|
|
167
|
+
def test_code_the_auditor_needs_is_kept(self) -> None:
|
|
168
|
+
for line in self.KEPT:
|
|
169
|
+
with self.subTest(line=line):
|
|
170
|
+
self.assertEqual(self.redact(line), line)
|
|
171
|
+
|
|
172
|
+
def test_long_lines_are_fast_and_not_sent(self) -> None:
|
|
173
|
+
lines = ["a.b" * 300000 + "password", "a-" * 500000 + "://", "a-b." * 250000, "x" * 1000000]
|
|
174
|
+
start = time.monotonic()
|
|
175
|
+
out = _redact_lines(lines)
|
|
176
|
+
self.assertLess(time.monotonic() - start, 2.0)
|
|
177
|
+
self.assertTrue(all(o.startswith("[line of ") for o in out))
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
if __name__ == "__main__":
|
|
181
|
+
unittest.main()
|