@hoanghainh/komagu 2.6.0 → 2.7.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 +23 -0
- package/README.md +1 -0
- package/assess/lsa/knowledge/path-ssrf-patterns.md +19 -0
- package/assess/lsa/knowledge/severity-and-cwe.md +3 -1
- package/assess/lsa/scripts/find_path_ssrf.py +176 -0
- package/assess/lsa/scripts/findings_to_sarif.py +29 -0
- package/assess/lsa/scripts/merge_findings.py +1 -1
- package/assess/lsa/scripts/run_prescan.py +4 -0
- package/assess/lsa/tests/run_tests.py +4 -0
- package/assess/lsa/tests/test_path_ssrf.py +94 -0
- package/contracts/schemas/finding.schema.json +4 -2
- package/examples/member-portal/backend-dotnet/Auth/DemoAuthenticationHandler.cs +75 -0
- package/examples/member-portal/backend-dotnet/Program.cs +1 -72
- package/examples/member-portal/backend-spring/src/main/kotlin/io/komagu/portal/Application.kt +4 -0
- package/examples/member-portal/backend-spring/src/main/kotlin/io/komagu/portal/controller/Controllers.kt +3 -0
- package/examples/member-portal/backend-spring/src/main/kotlin/io/komagu/portal/model/Entities.kt +4 -0
- package/examples/member-portal/tests/test_d6_demo_app.py +4 -4
- package/examples/member-portal/tests/test_d6_remediation_verification.py +6 -6
- package/operate/README.md +6 -1
- package/operate/ai/client.py +5 -1
- package/operate/cli/__init__.py +1 -1
- package/operate/cli/commands/fix.py +47 -0
- package/operate/cli/commands/mcp.py +54 -0
- package/operate/cli/main.py +42 -4
- package/operate/cross_user_tester/runner.py +4 -1
- package/operate/devsecops/sbom.py +12 -4
- package/operate/mcp/__init__.py +11 -0
- package/operate/mcp/server.py +206 -0
- package/operate/mcp/tools.py +464 -0
- package/operate/remediator/ai_fixer.py +285 -0
- package/operate/remediator/runner.py +11 -0
- package/operate/remediator/strategies.py +64 -0
- package/operate/simulate/scenarios.py +11 -2
- package/operate/tests/test_ai_remediation.py +288 -0
- package/operate/tests/test_audit_minimum_versions.py +89 -0
- package/operate/tests/test_cli.py +1 -1
- package/operate/tests/test_mcp_server.py +380 -0
- package/operate/tests/test_scheme_and_xml_guards.py +67 -0
- package/package.json +1 -1
- package/remediate/authz/bfla_patcher.py +319 -0
- package/remediate/authz/dotnet/src/Komagu.Security.Authorization/LoggingSecurityAuditSink.cs +19 -3
- package/remediate/authz/golang/authz_test.go +5 -2
- package/remediate/authz/mass_assignment_patcher.py +188 -0
- package/remediate/authz/patcher.py +8 -0
- package/remediate/authz/spring/src/main/kotlin/io/komagu/security/authorization/DefaultKomaguSecurityManager.kt +74 -78
- package/remediate/authz/spring/src/main/kotlin/io/komagu/security/spring/OwnershipAspect.kt +129 -158
- package/remediate/authz/tests/test_bfla_and_mass_assignment_patcher.py +213 -0
- package/remediate/deps/patcher.py +2 -1
- package/remediate/edge/mock-backend/app.py +2 -1
- package/remediate/edge/tests/test_edge_security.py +5 -5
- package/remediate/ekyc/requirements.txt +1 -1
- package/remediate/identity/dotnet/src/Komagu.Security.Identity/OidcServiceCollectionExtensions.cs +19 -6
- package/remediate/identity/python/requirements.txt +2 -2
- package/remediate/identity/spring/src/main/kotlin/io/komagu/security/identity/KomaguIdentityAutoConfiguration.kt +31 -38
- package/remediate/legacy_shims/tests/test_legacy_shims.py +8 -8
- package/remediate/query/dotnet/src/Komagu.Security.Query/SafeSortAllowList.cs +5 -2
- package/remediate/query/spring/src/main/kotlin/io/komagu/security/query/SafeSortAllowList.kt +10 -11
- package/remediate/query/spring/src/main/kotlin/io/komagu/security/query/detekt/StringBuiltSqlQueryRule.kt +19 -24
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Tests for Tier 2 AI-assisted multi-line remediation and verification gate (#394)."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import json
|
|
7
|
+
import shutil
|
|
8
|
+
import sys
|
|
9
|
+
import tempfile
|
|
10
|
+
import unittest
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
REPO_ROOT = Path(__file__).resolve().parents[2]
|
|
14
|
+
sys.path.insert(0, str(REPO_ROOT))
|
|
15
|
+
|
|
16
|
+
from operate.ai.mock_server import MockLLMHandler, start_mock_llm_server
|
|
17
|
+
from operate.cli.main import main as cli_main
|
|
18
|
+
from operate.remediator.ai_fixer import AITier2Fixer
|
|
19
|
+
from operate.remediator.runner import RemediationRunner
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class TestAITier2Fixer(unittest.TestCase):
|
|
23
|
+
"""Verify AI multi-line patching, redaction, and mandatory verification gates."""
|
|
24
|
+
|
|
25
|
+
@classmethod
|
|
26
|
+
def setUpClass(cls) -> None:
|
|
27
|
+
cls.server, cls.thread, cls.base_url = start_mock_llm_server()
|
|
28
|
+
|
|
29
|
+
@classmethod
|
|
30
|
+
def tearDownClass(cls) -> None:
|
|
31
|
+
cls.server.shutdown()
|
|
32
|
+
cls.server.server_close()
|
|
33
|
+
|
|
34
|
+
def setUp(self) -> None:
|
|
35
|
+
self.temp_dir = Path(tempfile.mkdtemp(prefix="komagu_ai_fix_test_")).resolve()
|
|
36
|
+
MockLLMHandler.response_generator = None
|
|
37
|
+
MockLLMHandler.received_requests = []
|
|
38
|
+
|
|
39
|
+
def tearDown(self) -> None:
|
|
40
|
+
shutil.rmtree(self.temp_dir, ignore_errors=True)
|
|
41
|
+
|
|
42
|
+
def test_java_legacy_multiline_sqli_fix_and_secret_redaction(self) -> None:
|
|
43
|
+
java_file = self.temp_dir / "OrderDao.java"
|
|
44
|
+
java_file.write_text(
|
|
45
|
+
"import java.sql.*;\n"
|
|
46
|
+
"public class OrderDao {\n"
|
|
47
|
+
' private String dbPassword = "SuperSecretPassword99";\n'
|
|
48
|
+
" public ResultSet find(Connection conn, String id) throws SQLException {\n"
|
|
49
|
+
" Statement st = conn.createStatement();\n"
|
|
50
|
+
' return st.executeQuery("SELECT * FROM orders WHERE id = \'" + id + "\'");\n'
|
|
51
|
+
" }\n"
|
|
52
|
+
"}\n",
|
|
53
|
+
encoding="utf-8",
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
def reply(messages, model):
|
|
57
|
+
return json.dumps(
|
|
58
|
+
{
|
|
59
|
+
"start_line": 5,
|
|
60
|
+
"end_line": 6,
|
|
61
|
+
"patched_snippet": (
|
|
62
|
+
' PreparedStatement ps = conn.prepareStatement("SELECT * FROM orders WHERE id = ?");\n'
|
|
63
|
+
" ps.setString(1, id);\n"
|
|
64
|
+
" return ps.executeQuery();\n"
|
|
65
|
+
),
|
|
66
|
+
"explanation": "Replaced Statement concatenation with PreparedStatement.",
|
|
67
|
+
}
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
MockLLMHandler.response_generator = reply
|
|
71
|
+
|
|
72
|
+
findings_path = self.temp_dir / "findings.json"
|
|
73
|
+
findings_path.write_text(
|
|
74
|
+
json.dumps(
|
|
75
|
+
{
|
|
76
|
+
"findings": [
|
|
77
|
+
{
|
|
78
|
+
"finding_id": "INJ-JAVA-01",
|
|
79
|
+
"category": "sql_injection",
|
|
80
|
+
"cwe": "CWE-89",
|
|
81
|
+
"title": "Dynamic SQL in OrderDao",
|
|
82
|
+
"location": {"file": "OrderDao.java", "start_line": 6},
|
|
83
|
+
}
|
|
84
|
+
]
|
|
85
|
+
}
|
|
86
|
+
),
|
|
87
|
+
encoding="utf-8",
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
fixer = AITier2Fixer(endpoint=self.base_url, model="mock-model")
|
|
91
|
+
runner = RemediationRunner(
|
|
92
|
+
target_dir=self.temp_dir,
|
|
93
|
+
findings_path=findings_path,
|
|
94
|
+
dry_run=True,
|
|
95
|
+
ai_fixer=fixer,
|
|
96
|
+
)
|
|
97
|
+
report = runner.run()
|
|
98
|
+
|
|
99
|
+
self.assertEqual(report.planned_count, 1)
|
|
100
|
+
plan = report.plans[0]
|
|
101
|
+
self.assertIn("[AI-GENERATED — REQUIRES PERSON B REVIEW]", plan.reason)
|
|
102
|
+
self.assertIn("PreparedStatement ps = conn.prepareStatement", plan.patched_content)
|
|
103
|
+
|
|
104
|
+
# Ensure secret was redacted before leaving the host
|
|
105
|
+
sent_user_msg = MockLLMHandler.received_requests[-1]["messages"][1]["content"]
|
|
106
|
+
self.assertNotIn("SuperSecretPassword99", sent_user_msg)
|
|
107
|
+
self.assertIn("[REDACTED]", sent_user_msg)
|
|
108
|
+
|
|
109
|
+
def test_php_legacy_multiline_sqli_fix(self) -> None:
|
|
110
|
+
php_file = self.temp_dir / "orders.php"
|
|
111
|
+
php_file.write_text(
|
|
112
|
+
"<?php\n"
|
|
113
|
+
"$id = $_GET['id'];\n"
|
|
114
|
+
'$res = mysql_query("SELECT * FROM orders WHERE id = " . $id);\n',
|
|
115
|
+
encoding="utf-8",
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
def reply(messages, model):
|
|
119
|
+
return json.dumps(
|
|
120
|
+
{
|
|
121
|
+
"start_line": 3,
|
|
122
|
+
"end_line": 3,
|
|
123
|
+
"patched_snippet": (
|
|
124
|
+
'$stmt = $pdo->prepare("SELECT * FROM orders WHERE id = ?");\n'
|
|
125
|
+
"$stmt->execute([$id]);\n"
|
|
126
|
+
"$res = $stmt->fetchAll();\n"
|
|
127
|
+
),
|
|
128
|
+
"explanation": "Migrated mysql_query concatenation to PDO prepared statement.",
|
|
129
|
+
}
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
MockLLMHandler.response_generator = reply
|
|
133
|
+
fixer = AITier2Fixer(endpoint=self.base_url, model="mock-model")
|
|
134
|
+
res = fixer.generate_verified_patch(
|
|
135
|
+
php_file,
|
|
136
|
+
php_file.read_text(encoding="utf-8"),
|
|
137
|
+
{"category": "sql_injection", "cwe": "CWE-89", "location": {"start_line": 3}},
|
|
138
|
+
)
|
|
139
|
+
self.assertTrue(res.changed)
|
|
140
|
+
self.assertTrue(res.verified)
|
|
141
|
+
self.assertIn("$pdo->prepare", res.patched_content)
|
|
142
|
+
|
|
143
|
+
def test_rejects_comment_only_patch(self) -> None:
|
|
144
|
+
java_file = self.temp_dir / "Legacy.java"
|
|
145
|
+
content = (
|
|
146
|
+
"public class Legacy {\n"
|
|
147
|
+
' String q = "SELECT * FROM users WHERE name = \'" + name + "\'";\n'
|
|
148
|
+
"}\n"
|
|
149
|
+
)
|
|
150
|
+
java_file.write_text(content, encoding="utf-8")
|
|
151
|
+
|
|
152
|
+
MockLLMHandler.response_generator = lambda m, mdl: json.dumps(
|
|
153
|
+
{
|
|
154
|
+
"start_line": 2,
|
|
155
|
+
"end_line": 2,
|
|
156
|
+
"patched_snippet": (
|
|
157
|
+
" // TODO: parameterize query\n"
|
|
158
|
+
' String q = "SELECT * FROM users WHERE name = \'" + name + "\'";\n'
|
|
159
|
+
),
|
|
160
|
+
"explanation": "Added comment.",
|
|
161
|
+
}
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
fixer = AITier2Fixer(endpoint=self.base_url, model="mock-model")
|
|
165
|
+
res = fixer.generate_verified_patch(
|
|
166
|
+
java_file,
|
|
167
|
+
content,
|
|
168
|
+
{"category": "sql_injection", "location": {"start_line": 2}},
|
|
169
|
+
)
|
|
170
|
+
self.assertFalse(res.changed)
|
|
171
|
+
self.assertIn("comment-only", res.explanation)
|
|
172
|
+
|
|
173
|
+
def test_rejects_python_syntax_error_patch(self) -> None:
|
|
174
|
+
py_file = self.temp_dir / "bad.py"
|
|
175
|
+
content = "def get_user(cur, uid):\n cur.execute(f'SELECT * FROM users WHERE id = {uid}')\n"
|
|
176
|
+
py_file.write_text(content, encoding="utf-8")
|
|
177
|
+
|
|
178
|
+
MockLLMHandler.response_generator = lambda m, mdl: json.dumps(
|
|
179
|
+
{
|
|
180
|
+
"start_line": 2,
|
|
181
|
+
"end_line": 2,
|
|
182
|
+
"patched_snippet": " cur.execute('SELECT * FROM users WHERE id = %s', (uid,)\n", # missing ')'
|
|
183
|
+
"explanation": "Broken syntax.",
|
|
184
|
+
}
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
fixer = AITier2Fixer(endpoint=self.base_url, model="mock-model")
|
|
188
|
+
res = fixer.generate_verified_patch(
|
|
189
|
+
py_file,
|
|
190
|
+
content,
|
|
191
|
+
{"category": "sql_injection", "location": {"start_line": 2}},
|
|
192
|
+
)
|
|
193
|
+
self.assertFalse(res.changed)
|
|
194
|
+
self.assertIn("py_compile failed", res.explanation)
|
|
195
|
+
|
|
196
|
+
def test_rejects_patch_that_leaves_injection_sink_intact(self) -> None:
|
|
197
|
+
java_file = self.temp_dir / "StillBad.java"
|
|
198
|
+
content = (
|
|
199
|
+
"public class StillBad {\n"
|
|
200
|
+
" void run(Statement st, String id) {\n"
|
|
201
|
+
' st.executeQuery("SELECT * FROM t WHERE id = " + id);\n'
|
|
202
|
+
" }\n"
|
|
203
|
+
"}\n"
|
|
204
|
+
)
|
|
205
|
+
java_file.write_text(content, encoding="utf-8")
|
|
206
|
+
|
|
207
|
+
MockLLMHandler.response_generator = lambda m, mdl: json.dumps(
|
|
208
|
+
{
|
|
209
|
+
"start_line": 3,
|
|
210
|
+
"end_line": 3,
|
|
211
|
+
"patched_snippet": ' st.executeQuery("SELECT * FROM t WHERE id = " + id.trim());\n',
|
|
212
|
+
"explanation": "Trimmed input but still concatenated.",
|
|
213
|
+
}
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
fixer = AITier2Fixer(endpoint=self.base_url, model="mock-model")
|
|
217
|
+
res = fixer.generate_verified_patch(
|
|
218
|
+
java_file,
|
|
219
|
+
content,
|
|
220
|
+
{"category": "sql_injection", "location": {"start_line": 3}},
|
|
221
|
+
)
|
|
222
|
+
self.assertFalse(res.changed)
|
|
223
|
+
self.assertIn("Tier 1 re-scan still flags", res.explanation)
|
|
224
|
+
|
|
225
|
+
def test_cli_fix_with_ai_flag_end_to_end(self) -> None:
|
|
226
|
+
java_file = self.temp_dir / "App.java"
|
|
227
|
+
java_file.write_text(
|
|
228
|
+
"import java.sql.*;\n"
|
|
229
|
+
"public class App {\n"
|
|
230
|
+
" void run(Connection c, String id) throws SQLException {\n"
|
|
231
|
+
" Statement st = c.createStatement();\n"
|
|
232
|
+
' st.executeQuery("SELECT * FROM items WHERE id = " + id);\n'
|
|
233
|
+
" }\n"
|
|
234
|
+
"}\n",
|
|
235
|
+
encoding="utf-8",
|
|
236
|
+
)
|
|
237
|
+
findings_path = self.temp_dir / "findings.json"
|
|
238
|
+
findings_path.write_text(
|
|
239
|
+
json.dumps(
|
|
240
|
+
{
|
|
241
|
+
"findings": [
|
|
242
|
+
{
|
|
243
|
+
"finding_id": "INJ-01",
|
|
244
|
+
"category": "sql_injection",
|
|
245
|
+
"cwe": "CWE-89",
|
|
246
|
+
"location": {"file": "App.java", "start_line": 5},
|
|
247
|
+
}
|
|
248
|
+
]
|
|
249
|
+
}
|
|
250
|
+
),
|
|
251
|
+
encoding="utf-8",
|
|
252
|
+
)
|
|
253
|
+
|
|
254
|
+
MockLLMHandler.response_generator = lambda m, mdl: json.dumps(
|
|
255
|
+
{
|
|
256
|
+
"start_line": 4,
|
|
257
|
+
"end_line": 5,
|
|
258
|
+
"patched_snippet": (
|
|
259
|
+
' PreparedStatement ps = c.prepareStatement("SELECT * FROM items WHERE id = ?");\n'
|
|
260
|
+
" ps.setString(1, id);\n"
|
|
261
|
+
" ps.executeQuery();\n"
|
|
262
|
+
),
|
|
263
|
+
"explanation": "Use PreparedStatement.",
|
|
264
|
+
}
|
|
265
|
+
)
|
|
266
|
+
|
|
267
|
+
exit_code = cli_main(
|
|
268
|
+
[
|
|
269
|
+
"--no-banner",
|
|
270
|
+
"fix",
|
|
271
|
+
str(self.temp_dir),
|
|
272
|
+
"--findings",
|
|
273
|
+
str(findings_path),
|
|
274
|
+
"--dry-run",
|
|
275
|
+
"--ai",
|
|
276
|
+
"--ai-endpoint",
|
|
277
|
+
self.base_url,
|
|
278
|
+
"--ai-model",
|
|
279
|
+
"mock-model",
|
|
280
|
+
"--format",
|
|
281
|
+
"json",
|
|
282
|
+
]
|
|
283
|
+
)
|
|
284
|
+
self.assertEqual(exit_code, 0)
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
if __name__ == "__main__":
|
|
288
|
+
unittest.main()
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""`.github/scripts/audit_minimum_versions.py`: audits the lowest version each requirement allows (#390)."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import importlib.util
|
|
7
|
+
import os
|
|
8
|
+
import tempfile
|
|
9
|
+
import unittest
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from unittest import mock
|
|
12
|
+
|
|
13
|
+
SCRIPT = Path(__file__).resolve().parents[2] / ".github" / "scripts" / "audit_minimum_versions.py"
|
|
14
|
+
_spec = importlib.util.spec_from_file_location("audit_minimum_versions", SCRIPT)
|
|
15
|
+
amv = importlib.util.module_from_spec(_spec)
|
|
16
|
+
_spec.loader.exec_module(amv)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _report(*vulns: tuple) -> dict:
|
|
20
|
+
deps = {}
|
|
21
|
+
for name, version, vuln_id, fixes in vulns:
|
|
22
|
+
deps.setdefault((name, version), []).append({"id": vuln_id, "fix_versions": fixes})
|
|
23
|
+
return {"dependencies": [{"name": n, "version": v, "vulns": vs} for (n, v), vs in deps.items()]}
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class TestMinimumPins(unittest.TestCase):
|
|
27
|
+
def test_lower_bounds_and_exact_pins_become_pins(self) -> None:
|
|
28
|
+
text = "# comment\npyjwt>=2.15.0\ncryptography[ssh] >= 50.0.0 # note\nrequests==2.32.3\n"
|
|
29
|
+
self.assertEqual(amv.minimum_pins(text), ["pyjwt==2.15.0", "cryptography==50.0.0", "requests==2.32.3"])
|
|
30
|
+
|
|
31
|
+
def test_unversioned_and_upper_bounds_are_ignored(self) -> None:
|
|
32
|
+
self.assertEqual(amv.minimum_pins("flask\nurllib3<3\n-r other.txt\n"), [])
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class TestClassify(unittest.TestCase):
|
|
36
|
+
def test_fixable_and_unfixable_are_split_and_deduplicated(self) -> None:
|
|
37
|
+
report = _report(
|
|
38
|
+
("pyjwt", "2.8.0", "PYSEC-1", ["2.12.0"]),
|
|
39
|
+
("pyjwt", "2.8.0", "PYSEC-1", ["2.12.0"]),
|
|
40
|
+
("pyjwt", "2.8.0", "PYSEC-2", []),
|
|
41
|
+
)
|
|
42
|
+
fixable, unfixable = amv.classify(report)
|
|
43
|
+
self.assertEqual(fixable, ["pyjwt 2.8.0: PYSEC-1 (fixed in 2.12.0)"])
|
|
44
|
+
self.assertEqual(unfixable, ["pyjwt 2.8.0: PYSEC-2 (no fixed version)"])
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class TestMain(unittest.TestCase):
|
|
48
|
+
def setUp(self) -> None:
|
|
49
|
+
self.tmp = tempfile.mkdtemp(prefix="komagu_amv_")
|
|
50
|
+
self.req = os.path.join(self.tmp, "requirements.txt")
|
|
51
|
+
self.summary = os.path.join(self.tmp, "summary.md")
|
|
52
|
+
with open(self.req, "w", encoding="utf-8") as handle:
|
|
53
|
+
handle.write("pyjwt>=2.8.0\n")
|
|
54
|
+
|
|
55
|
+
def _run(self, report: dict) -> int:
|
|
56
|
+
with mock.patch.object(amv, "audit", return_value=report) as audit, \
|
|
57
|
+
mock.patch("builtins.print"):
|
|
58
|
+
code = amv.main(["--summary", self.summary, self.req])
|
|
59
|
+
audit.assert_called_once_with(["pyjwt==2.8.0"])
|
|
60
|
+
return code
|
|
61
|
+
|
|
62
|
+
def test_fixable_vulnerability_at_the_minimum_fails(self) -> None:
|
|
63
|
+
self.assertEqual(self._run(_report(("pyjwt", "2.8.0", "PYSEC-1", ["2.12.0"]))), 1)
|
|
64
|
+
with open(self.summary, encoding="utf-8") as handle:
|
|
65
|
+
self.assertIn("FAIL pyjwt 2.8.0: PYSEC-1", handle.read())
|
|
66
|
+
|
|
67
|
+
def test_vulnerability_without_a_fix_is_only_reported(self) -> None:
|
|
68
|
+
self.assertEqual(self._run(_report(("pyjwt", "2.8.0", "PYSEC-2", []))), 0)
|
|
69
|
+
with open(self.summary, encoding="utf-8") as handle:
|
|
70
|
+
self.assertIn("note pyjwt 2.8.0: PYSEC-2", handle.read())
|
|
71
|
+
|
|
72
|
+
def test_clean_minimum_passes(self) -> None:
|
|
73
|
+
self.assertEqual(self._run({"dependencies": [{"name": "pyjwt", "version": "2.8.0", "vulns": []}]}), 0)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class TestRepositoryMinimums(unittest.TestCase):
|
|
77
|
+
"""Minimums raised in #390: these versions had fixable advisories."""
|
|
78
|
+
|
|
79
|
+
def test_raised_minimums(self) -> None:
|
|
80
|
+
root = SCRIPT.parents[2]
|
|
81
|
+
ekyc = amv.minimum_pins((root / "remediate/ekyc/requirements.txt").read_text(encoding="utf-8"))
|
|
82
|
+
identity = amv.minimum_pins((root / "remediate/identity/python/requirements.txt").read_text(encoding="utf-8"))
|
|
83
|
+
self.assertIn("cryptography==50.0.0", ekyc)
|
|
84
|
+
self.assertIn("cryptography==50.0.0", identity)
|
|
85
|
+
self.assertIn("pyjwt==2.15.0", identity)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
if __name__ == "__main__":
|
|
89
|
+
unittest.main()
|
|
@@ -61,7 +61,7 @@ class TestKomaguCLI(unittest.TestCase):
|
|
|
61
61
|
def test_subcommand_parser_builds_cleanly(self) -> None:
|
|
62
62
|
"""In-process parser must parse subcommands without throwing."""
|
|
63
63
|
parser = build_parser()
|
|
64
|
-
args_scan = parser.parse_args(["scan", "
|
|
64
|
+
args_scan = parser.parse_args(["scan", "./src", "--format", "html"])
|
|
65
65
|
self.assertEqual(args_scan.subcommand, "scan")
|
|
66
66
|
self.assertEqual(args_scan.format, "html")
|
|
67
67
|
|