@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
@@ -0,0 +1,453 @@
1
+ #!/usr/bin/env python3
2
+ """Tier 1 gaps found by comparing the pre-scan with real Java legacy applications (Issue #364).
3
+
4
+ Each test is a minimal reproduction of a pattern observed in a public application:
5
+ Apache Roller, Apache Struts 1, JEECG, JeeSite 1.x and OpenMRS 1.9. No third-party code is copied here.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import sys
11
+ import tempfile
12
+ import unittest
13
+ from pathlib import Path
14
+ from typing import Dict, List
15
+
16
+ REPO_ROOT = Path(__file__).resolve().parents[3]
17
+ sys.path.insert(0, str(REPO_ROOT / "assess" / "lsa" / "scripts"))
18
+
19
+ from detect_stack import detect # noqa: E402
20
+ from find_injection import find_injection # noqa: E402
21
+ from lsa_common import iter_source_files, load_config # noqa: E402
22
+ from map_routes import map_routes # noqa: E402
23
+
24
+ CONFIG_XML_HEADER = '<?xml version="1.0"?>\n'
25
+
26
+
27
+ def write_repo(root: Path, files: Dict[str, str]) -> None:
28
+ for rel, text in files.items():
29
+ path = root / rel
30
+ path.parent.mkdir(parents=True, exist_ok=True)
31
+ path.write_text(text, encoding="utf-8")
32
+
33
+
34
+ def scan_routes(files: Dict[str, str]) -> List[Dict]:
35
+ with tempfile.TemporaryDirectory() as td:
36
+ root = Path(td)
37
+ write_repo(root, files)
38
+ return map_routes(root, load_config(None), workers=1)["candidates"]
39
+
40
+
41
+ def scan_stack(files: Dict[str, str]) -> Dict:
42
+ with tempfile.TemporaryDirectory() as td:
43
+ root = Path(td)
44
+ write_repo(root, files)
45
+ return detect(root, load_config(None))
46
+
47
+
48
+ def scan_injection(files: Dict[str, str]) -> Dict:
49
+ with tempfile.TemporaryDirectory() as td:
50
+ root = Path(td)
51
+ write_repo(root, files)
52
+ return find_injection(root, load_config(None), workers=1)
53
+
54
+
55
+ def scoped_files(files: Dict[str, str]) -> List[str]:
56
+ with tempfile.TemporaryDirectory() as td:
57
+ root = Path(td)
58
+ write_repo(root, files)
59
+ return [rel for _, rel in iter_source_files(root, load_config(None))]
60
+
61
+
62
+ def paths_of(routes: List[Dict], stack: str) -> List[str]:
63
+ return sorted(r["path"] for r in routes if r["stack"] == stack)
64
+
65
+
66
+ def controller(body: str, class_annotations: str = "") -> Dict[str, str]:
67
+ source = (
68
+ "package com.acme.web;\n"
69
+ "@Controller\n"
70
+ f"{class_annotations}\n"
71
+ "public class ItemController {\n"
72
+ f"{body}\n"
73
+ "}\n"
74
+ )
75
+ return {"src/main/java/com/acme/web/ItemController.java": source}
76
+
77
+
78
+ class TestScope(unittest.TestCase):
79
+ def test_java_package_named_like_a_build_directory_is_scanned(self) -> None:
80
+ scanned = scoped_files({
81
+ "src/main/java/com/acme/vendor/VendorController.java": "class A {}\n",
82
+ "src/main/java/com/acme/modules/test/web/TestController.java": "class B {}\n",
83
+ "src/com/acme/build/BuildDao.java": "class C {}\n",
84
+ })
85
+ self.assertEqual(len(scanned), 3)
86
+
87
+ def test_jsp_in_a_web_directory_named_test_is_scanned(self) -> None:
88
+ scanned = scoped_files({"src/main/webapp/test/mailer_test.jsp": "<html/>\n"})
89
+ self.assertEqual(scanned, ["src/main/webapp/test/mailer_test.jsp"])
90
+
91
+ def test_real_test_and_vendor_trees_stay_excluded(self) -> None:
92
+ scanned = scoped_files({
93
+ "src/test/java/com/acme/FooTest.java": "class T {}\n",
94
+ "test/com/acme/BarTest.java": "class U {}\n",
95
+ "target/classes/com/acme/Baz.java": "class V {}\n",
96
+ "src/main/webapp/test/helper.js": "var a = 1;\n",
97
+ "src/main/webapp/static/vendor/lib.js": "var b = 1;\n",
98
+ })
99
+ self.assertEqual(scanned, [])
100
+
101
+
102
+ class TestStackDetection(unittest.TestCase):
103
+ def test_java_ternary_is_not_dotnet_mvc(self) -> None:
104
+ profile = scan_stack({"src/A.java": "class A { Object c = ok ? other : Controller.NONE; }\n"})
105
+ self.assertNotIn("dotnet-mvc", [s["id"] for s in profile["stacks"]])
106
+
107
+ def test_commented_import_is_not_struts2(self) -> None:
108
+ profile = scan_stack({"src/A.java": "//import com.opensymphony.xwork2.ognl.OgnlUtil;\nclass A {}\n"})
109
+ self.assertNotIn("java-struts2", [s["id"] for s in profile["stacks"]])
110
+
111
+ def test_hibernate_validator_is_not_the_orm(self) -> None:
112
+ profile = scan_stack({"src/A.java": "import org.hibernate.validator.constraints.Length;\nclass A {}\n"})
113
+ self.assertNotIn("java-hibernate", [s["id"] for s in profile["stacks"]])
114
+ self.assertNotIn("Hibernate/JPA", profile["data_access"])
115
+
116
+ def test_mybatis_sql_command_type_is_not_ado_net(self) -> None:
117
+ profile = scan_stack({"src/A.java": "class A { SqlCommandType type; }\n"})
118
+ self.assertNotIn("ADO.NET", profile["data_access"])
119
+
120
+ def test_real_dotnet_and_orm_still_detected(self) -> None:
121
+ profile = scan_stack({
122
+ "Web/HomeController.cs": "class HomeController : Controller { SqlCommand c; }\n",
123
+ "src/A.java": "import org.hibernate.Session;\nclass A {}\n",
124
+ })
125
+ self.assertIn("dotnet-mvc", [s["id"] for s in profile["stacks"]])
126
+ self.assertIn("java-hibernate", [s["id"] for s in profile["stacks"]])
127
+ self.assertIn("ADO.NET", profile["data_access"])
128
+
129
+
130
+ class TestXmlRoutes(unittest.TestCase):
131
+ def test_commented_out_mappings_are_not_routes(self) -> None:
132
+ web = CONFIG_XML_HEADER + (
133
+ "<web-app>\n<!--\n<servlet><servlet-name>cxf</servlet-name><servlet-class>a.Cxf</servlet-class></servlet>\n"
134
+ "<servlet-mapping><servlet-name>cxf</servlet-name><url-pattern>/cxf/*</url-pattern></servlet-mapping>\n-->\n</web-app>\n"
135
+ )
136
+ struts = CONFIG_XML_HEADER + (
137
+ '<struts-config><action-mappings><action path="/live" type="a.Live"/>\n'
138
+ '<!-- <action path="/dead" type="a.Dead"/> -->\n</action-mappings></struts-config>\n'
139
+ )
140
+ routes = scan_routes({"WEB-INF/web.xml": web, "WEB-INF/struts-config.xml": struts})
141
+ self.assertEqual(paths_of(routes, "java-servlet"), [])
142
+ self.assertEqual(paths_of(routes, "java-struts1"), ["/live.do"])
143
+
144
+ def test_servlet_with_description_and_several_url_patterns(self) -> None:
145
+ web = CONFIG_XML_HEADER + (
146
+ "<web-app><servlet><description>x</description><servlet-name>s</servlet-name><servlet-class>com.acme.Img</servlet-class></servlet>\n"
147
+ "<servlet-mapping><servlet-name>s</servlet-name><url-pattern>/a/*</url-pattern><url-pattern>/b/*</url-pattern></servlet-mapping>\n"
148
+ "<servlet-mapping><servlet-name>s</servlet-name><url-pattern>/c/*</url-pattern></servlet-mapping></web-app>\n"
149
+ )
150
+ servlet = "package com.acme;\npublic class Img extends HttpServlet {\n public void doGet(HttpServletRequest r) {}\n}\n"
151
+ routes = scan_routes({"WEB-INF/web.xml": web, "src/com/acme/Img.java": servlet})
152
+ self.assertEqual(paths_of(routes, "java-servlet"), ["/a/*", "/b/*", "/c/*"])
153
+ self.assertTrue(all(r["handler"]["file"].endswith("Img.java") for r in routes))
154
+
155
+ def test_handler_is_resolved_by_package_not_simple_name(self) -> None:
156
+ struts = CONFIG_XML_HEADER + (
157
+ '<struts-config><action-mappings><action path="/a" type="com.acme.one.ListAction"/>'
158
+ '<action path="/b" type="com.acme.two.ListAction"/>'
159
+ '<action path="/lib" type="org.other.ListAction"/></action-mappings></struts-config>\n'
160
+ )
161
+ routes = scan_routes({
162
+ "WEB-INF/struts-config.xml": struts,
163
+ "src/com/acme/one/ListAction.java": "package com.acme.one;\nclass ListAction {}\n",
164
+ "src/com/acme/two/ListAction.java": "package com.acme.two;\nclass ListAction {}\n",
165
+ })
166
+ handlers = {r["path"]: r["handler"]["file"] for r in routes}
167
+ self.assertEqual(handlers["/a.do"], "src/com/acme/one/ListAction.java")
168
+ self.assertEqual(handlers["/b.do"], "src/com/acme/two/ListAction.java")
169
+ self.assertEqual(handlers["/lib.do"], "WEB-INF/struts-config.xml")
170
+
171
+ def test_struts2_included_file_and_custom_extension(self) -> None:
172
+ main = CONFIG_XML_HEADER + (
173
+ "<struts><include file=\"struts-order.xml\"/>\n"
174
+ '<package name="m" namespace="/m"><action name="home" class="a.Home"/></package></struts>\n'
175
+ )
176
+ included = (
177
+ CONFIG_XML_HEADER
178
+ + '<!DOCTYPE struts PUBLIC "x" "y">\n<struts><package name="o" namespace="/o"><action name="view" class="a.View"/></package></struts>\n'
179
+ )
180
+ routes = scan_routes({
181
+ "src/main/resources/struts.xml": main,
182
+ "src/main/resources/struts-order.xml": included,
183
+ "src/main/resources/struts.properties": "struts.action.extension=rol\n",
184
+ })
185
+ self.assertEqual(paths_of(routes, "java-struts2"), ["/m/home.rol", "/o/view.rol"])
186
+
187
+ def test_struts2_extension_from_constant_with_blank_alternative(self) -> None:
188
+ xml = CONFIG_XML_HEADER + (
189
+ '<struts><constant name="struts.action.extension" value="do,,"/>\n'
190
+ '<package name="m" namespace="/m"><action name="home" class="a.Home"/></package></struts>\n'
191
+ )
192
+ routes = scan_routes({"src/main/resources/struts.xml": xml})
193
+ self.assertEqual(paths_of(routes, "java-struts2"), ["/m/home.do"])
194
+
195
+ def test_struts1_module_prefix_comes_from_web_xml(self) -> None:
196
+ web = CONFIG_XML_HEADER + (
197
+ "<web-app><servlet><servlet-name>a</servlet-name><servlet-class>org.apache.struts.action.ActionServlet</servlet-class>\n"
198
+ "<init-param><param-name>config/admin</param-name><param-value>/WEB-INF/admin/struts-config.xml</param-value></init-param>\n"
199
+ "</servlet></web-app>\n"
200
+ )
201
+ admin = CONFIG_XML_HEADER + '<struts-config><action-mappings><action path="/edit" type="a.Edit"/></action-mappings></struts-config>\n'
202
+ root = CONFIG_XML_HEADER + '<struts-config><action-mappings><action path="/home" type="a.Home"/></action-mappings></struts-config>\n'
203
+ routes = scan_routes({
204
+ "app/WEB-INF/web.xml": web,
205
+ "app/WEB-INF/admin/struts-config.xml": admin,
206
+ "app/WEB-INF/struts-config.xml": root,
207
+ })
208
+ self.assertEqual(paths_of(routes, "java-struts1"), ["/admin/edit.do", "/home.do"])
209
+
210
+ def test_spring_simple_url_handler_mapping(self) -> None:
211
+ servlet = CONFIG_XML_HEADER + (
212
+ '<beans><bean id="urlMapping" class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping">\n'
213
+ '<property name="interceptors"><list><bean class="com.acme.Audit"></bean></list></property>\n'
214
+ '<property name="mappings"><props><prop key="/admin/order.form">orderForm</prop></props></property></bean>\n'
215
+ '<bean id="orderForm" class="com.acme.OrderFormController"/>\n</beans>\n'
216
+ )
217
+ routes = scan_routes({
218
+ "WEB-INF/app-servlet.xml": servlet,
219
+ "src/com/acme/OrderFormController.java": 'package com.acme;\nclass OrderFormController { void f() { getParameter("orderId"); } }\n',
220
+ })
221
+ spring = [r for r in routes if r["stack"] == "java-spring-mvc"]
222
+ self.assertEqual([r["path"] for r in spring], ["/admin/order.form"])
223
+ self.assertEqual(spring[0]["handler"]["file"], "src/com/acme/OrderFormController.java")
224
+ self.assertIn("orderId", [p["name"] for p in spring[0]["id_params"]])
225
+
226
+
227
+ class TestSpringAnnotations(unittest.TestCase):
228
+ def test_annotation_between_modifier_and_return_type(self) -> None:
229
+ routes = scan_routes(controller(
230
+ ' @RequestMapping("/q/h")\n public @ResponseBody\n List<Map<String, Object>> list(@RequestParam("itemId") int id) { return null; }\n'
231
+ ))
232
+ self.assertEqual(paths_of(routes, "java-spring-mvc"), ["/q/h"])
233
+ self.assertEqual(routes[0]["handler"]["symbol"], "list")
234
+
235
+ def test_array_of_paths_gives_one_route_each(self) -> None:
236
+ routes = scan_routes(controller(
237
+ ' @RequestMapping(value = {"list", ""})\n public String list() { return ""; }\n',
238
+ '@RequestMapping(value = "/sys/user")',
239
+ ))
240
+ self.assertEqual(paths_of(routes, "java-spring-mvc"), ["/sys/user", "/sys/user/list"])
241
+
242
+ def test_params_is_a_query_string_not_a_path(self) -> None:
243
+ routes = scan_routes(controller(
244
+ ' @RequestMapping(params = "doDel")\n public String del() { return ""; }\n'
245
+ ' @RequestMapping(value = "/x", params = "action=up")\n public String up() { return ""; }\n',
246
+ '@RequestMapping("/item")',
247
+ ))
248
+ self.assertEqual(paths_of(routes, "java-spring-mvc"), ["/item/x?action=up", "/item?doDel"])
249
+
250
+ def test_produces_is_not_a_path(self) -> None:
251
+ routes = scan_routes(controller(
252
+ ' @RequestMapping(method = RequestMethod.POST, produces = "application/json")\n public String p() { return ""; }\n',
253
+ '@RequestMapping("/item")',
254
+ ))
255
+ self.assertEqual(paths_of(routes, "java-spring-mvc"), ["/item"])
256
+ self.assertEqual(routes[0]["method"], ["POST"])
257
+
258
+ def test_class_literal_in_a_method_does_not_become_the_class_prefix(self) -> None:
259
+ routes = scan_routes(controller(
260
+ ' @RequestMapping(value = "export")\n public String e() { new Excel(User.class); return ""; }\n'
261
+ ' @RequestMapping(value = "info")\n public String i() { return ""; }\n',
262
+ '@RequestMapping(value = "/sys/user")',
263
+ ))
264
+ self.assertEqual(paths_of(routes, "java-spring-mvc"), ["/sys/user/export", "/sys/user/info"])
265
+
266
+ def test_string_constant_path_is_resolved(self) -> None:
267
+ routes = scan_routes(controller(
268
+ ' private static final String PATH = "/admin/settings";\n'
269
+ ' @RequestMapping(value = PATH + "/save", method = RequestMethod.POST)\n public String s() { return ""; }\n'
270
+ ))
271
+ self.assertEqual(paths_of(routes, "java-spring-mvc"), ["/admin/settings/save"])
272
+
273
+ def test_placeholder_is_not_an_id_parameter(self) -> None:
274
+ routes = scan_routes(controller(
275
+ ' @RequestMapping(value = "{tenantId}/view")\n public String v() { return ""; }\n',
276
+ '@RequestMapping(value = "${adminPath}/sys/user")',
277
+ ))
278
+ self.assertEqual([p["name"] for p in routes[0]["id_params"]], ["tenantId"])
279
+
280
+ def test_commented_out_annotation_is_not_a_route(self) -> None:
281
+ routes = scan_routes(controller(
282
+ ' // @RequestMapping(value = "old")\n public String old() { return ""; }\n'
283
+ ' /*\n @RequestMapping(value = "dead")\n public String dead() { return ""; }\n */\n'
284
+ ' @RequestMapping(value = "live")\n public String live() { return ""; }\n',
285
+ '@RequestMapping("/item")',
286
+ ))
287
+ self.assertEqual(paths_of(routes, "java-spring-mvc"), ["/item/live"])
288
+
289
+
290
+ class TestJspEntryPoints(unittest.TestCase):
291
+ def test_jsp_under_web_inf_is_not_a_route(self) -> None:
292
+ routes = scan_routes({"app/src/main/webapp/WEB-INF/jsps/editor/Entries.jsp": "<html/>\n"})
293
+ self.assertEqual(paths_of(routes, "java-jsp"), [])
294
+
295
+ def test_public_jsp_in_an_include_like_directory_is_a_route(self) -> None:
296
+ routes = scan_routes({"src/main/webapp/webpage/cgform/config/list.jsp": "<html/>\n"})
297
+ self.assertEqual(paths_of(routes, "java-jsp"), ["/webpage/cgform/config/list.jsp"])
298
+
299
+ def test_path_is_relative_to_the_web_root(self) -> None:
300
+ routes = scan_routes({"app/WebContent/order/detail.jsp": "<html/>\n"})
301
+ self.assertEqual(paths_of(routes, "java-jsp"), ["/order/detail.jsp"])
302
+
303
+
304
+ class TestInjectionPrecision(unittest.TestCase):
305
+ def test_html_select_builder_is_not_sql(self) -> None:
306
+ result = scan_injection({"src/A.java": (
307
+ "class A {\n"
308
+ " String f(String v) {\n"
309
+ " StringBuffer sb = new StringBuffer();\n"
310
+ ' sb.append("<select name=\\"" + v + "\\">");\n'
311
+ ' sb.append("<option value=\\"" + v + "\\">" + v);\n'
312
+ " return sb.toString();\n"
313
+ " }\n"
314
+ "}\n"
315
+ )})
316
+ self.assertEqual(result["candidates"], [])
317
+
318
+ def test_freemarker_generator_template_is_not_a_mapper(self) -> None:
319
+ result = scan_injection({"src/main/resources/templates/gen/mapper.xml": (
320
+ '<template><content><![CDATA[\n<mapper namespace="${pkg}">\n<select id="x">\n'
321
+ "SELECT * FROM ${table.name} <#if flag>WHERE a = 1</#if>\n</select></mapper>\n]]></content></template>\n"
322
+ )})
323
+ self.assertEqual(result["candidates"], [])
324
+
325
+ def test_real_mapper_dollar_still_flagged(self) -> None:
326
+ result = scan_injection({"src/ItemDao.xml": (
327
+ '<mapper namespace="a.ItemDao">\n<select id="x">\nSELECT * FROM item ORDER BY ${page.orderBy}\n</select></mapper>\n'
328
+ )})
329
+ self.assertEqual([c["rule"] for c in result["candidates"]], ["mybatis-dollar"])
330
+
331
+ def test_positional_parameter_index_is_suppressed(self) -> None:
332
+ result = scan_injection({"src/A.java": (
333
+ "class A {\n"
334
+ " String f(int size, String sortBy) {\n"
335
+ ' StringBuilder q = new StringBuilder("SELECT e FROM Entry e WHERE ");\n'
336
+ ' q.append(" AND e.name = ?").append(size);\n'
337
+ ' q.append(" ORDER BY ").append(sortBy);\n'
338
+ " return q.toString();\n"
339
+ " }\n"
340
+ "}\n"
341
+ )})
342
+ self.assertEqual([c["line"] for c in result["candidates"]], [5])
343
+ self.assertEqual([s["line"] for s in result["suppressed"]], [4])
344
+
345
+
346
+ class TestInjectionRecall(unittest.TestCase):
347
+ def test_operand_on_a_continuation_line_is_flagged(self) -> None:
348
+ result = scan_injection({"src/A.java": (
349
+ "class A {\n"
350
+ " void f(Connection c, String name) throws Exception {\n"
351
+ ' c.prepareStatement("INSERT INTO t (a) VALUES("\n'
352
+ " + name + \",?)\");\n"
353
+ " }\n"
354
+ "}\n"
355
+ )})
356
+ self.assertEqual([c["line"] for c in result["candidates"]], [4])
357
+
358
+ def test_leading_operand_call_on_a_continuation_line_is_flagged(self) -> None:
359
+ result = scan_injection({"src/A.java": (
360
+ "class A {\n"
361
+ " int f(Connection c, Name n) {\n"
362
+ ' return get(c, "SELECT count(*) FROM concept WHERE locale = \'"\n'
363
+ " + quote(n.getLocale())\n"
364
+ ' + "\'");\n'
365
+ " }\n"
366
+ "}\n"
367
+ )})
368
+ self.assertIn(4, [c["line"] for c in result["candidates"]])
369
+
370
+ def test_constant_on_a_continuation_line_is_not_flagged(self) -> None:
371
+ result = scan_injection({"src/A.java": (
372
+ "class A {\n"
373
+ " int f(Connection c) {\n"
374
+ ' return get(c, "SELECT count(*) FROM concept WHERE kind = \'"\n'
375
+ " + ConceptNameType.FULLY_SPECIFIED\n"
376
+ ' + "\'");\n'
377
+ " }\n"
378
+ "}\n"
379
+ )})
380
+ self.assertEqual(result["candidates"], [])
381
+
382
+ def test_jpql_literal_ending_in_where_is_flagged(self) -> None:
383
+ result = scan_injection({"src/A.java": (
384
+ "class A {\n"
385
+ " String f(StringBuilder whereClause) {\n"
386
+ ' return "FROM MediaFile m WHERE " + whereClause;\n'
387
+ " }\n"
388
+ "}\n"
389
+ )})
390
+ self.assertEqual([c["line"] for c in result["candidates"]], [3])
391
+
392
+
393
+ class XmlDeclaredHandlerLineTests(unittest.TestCase):
394
+ """The headless auditor reads 15 lines around handler.line, so it must point at the entry method (#374)."""
395
+
396
+ IMPORTS = "".join(f"import a.b.C{n};\n" for n in range(30))
397
+
398
+ def handler_line(self, config_rel: str, config: str, source_rel: str, source: str) -> int:
399
+ routes = scan_routes({config_rel: CONFIG_XML_HEADER + config, source_rel: source})
400
+ return next(r["handler"]["line"] for r in routes if r["handler"]["file"] == source_rel)
401
+
402
+ def test_struts1_action_points_at_execute(self) -> None:
403
+ source = "package a;\n" + self.IMPORTS + (
404
+ "public class ViewAction extends Action {\n"
405
+ " public ActionForward execute(ActionMapping m, ActionForm f,\n"
406
+ " HttpServletRequest request, HttpServletResponse response) {\n"
407
+ ' return load(request.getParameter("id"));\n'
408
+ " }\n"
409
+ "}\n"
410
+ )
411
+ line = self.handler_line(
412
+ "WEB-INF/struts-config.xml",
413
+ '<struts-config><action-mappings><action path="/view" type="a.ViewAction"/></action-mappings></struts-config>',
414
+ "src/a/ViewAction.java", source)
415
+ self.assertEqual(line, 33)
416
+
417
+ def test_struts2_action_points_at_the_declared_method(self) -> None:
418
+ source = "package a;\n" + self.IMPORTS + (
419
+ "public class ViewAction extends ActionSupport {\n"
420
+ " public String execute() { return list(); }\n"
421
+ " public String show() { return load(id); }\n"
422
+ "}\n"
423
+ )
424
+ line = self.handler_line(
425
+ "src/struts.xml",
426
+ '<struts><package name="p"><action name="view" class="a.ViewAction" method="show"/></package></struts>',
427
+ "src/a/ViewAction.java", source)
428
+ self.assertEqual(line, 34)
429
+
430
+ def test_servlet_from_web_xml_points_at_do_post(self) -> None:
431
+ source = "package a;\n" + self.IMPORTS + (
432
+ "public class ViewServlet extends HttpServlet {\n"
433
+ " protected void doPost(HttpServletRequest req, HttpServletResponse res) { load(req); }\n"
434
+ "}\n"
435
+ )
436
+ line = self.handler_line(
437
+ "WEB-INF/web.xml",
438
+ "<web-app><servlet><servlet-name>v</servlet-name><servlet-class>a.ViewServlet</servlet-class></servlet>"
439
+ "<servlet-mapping><servlet-name>v</servlet-name><url-pattern>/view</url-pattern></servlet-mapping></web-app>",
440
+ "src/a/ViewServlet.java", source)
441
+ self.assertEqual(line, 33)
442
+
443
+ def test_class_without_a_known_entry_method_keeps_line_one(self) -> None:
444
+ source = "package a;\npublic class Helper {\n void run() {}\n}\n"
445
+ line = self.handler_line(
446
+ "WEB-INF/struts-config.xml",
447
+ '<struts-config><action-mappings><action path="/h" type="a.Helper"/></action-mappings></struts-config>',
448
+ "src/a/Helper.java", source)
449
+ self.assertEqual(line, 1)
450
+
451
+
452
+ if __name__ == "__main__":
453
+ unittest.main()
@@ -2,7 +2,7 @@
2
2
  "$schema": "https://json-schema.org/draft/2020-12/schema",
3
3
  "$id": "operate/persona-suite.schema.json",
4
4
  "title": "Komagu test suite v2 (persona matrix)",
5
- "description": "Personas and, per route, the personas allowed to call it. Every other persona must be refused (BFLA). Tokens come only from environment variables.",
5
+ "description": "Personas and, per route, the personas allowed to call it. Every other persona must be refused (BFLA). Tokens come only from environment variables; session cookies come from an environment variable or a file.",
6
6
  "type": "object",
7
7
  "required": [
8
8
  "version",
@@ -29,6 +29,15 @@
29
29
  "pattern": "^[A-Za-z_][A-Za-z0-9_]*$",
30
30
  "description": "Environment variable that holds this persona's bearer token. Omit for an anonymous persona."
31
31
  },
32
+ "cookie_env": {
33
+ "type": "string",
34
+ "pattern": "^[A-Za-z_][A-Za-z0-9_]*$",
35
+ "description": "Environment variable that holds this persona's Cookie value, e.g. JSESSIONID=... Use for session-based apps instead of token_env."
36
+ },
37
+ "cookie_file": {
38
+ "type": "string",
39
+ "description": "Path to a file that holds this persona's Cookie value."
40
+ },
32
41
  "description": {
33
42
  "type": "string"
34
43
  }
@@ -75,7 +84,7 @@
75
84
  },
76
85
  "payload": {
77
86
  "type": "object",
78
- "description": "JSON body for writes."
87
+ "description": "Body fields for writes, encoded as set by body_format."
79
88
  },
80
89
  "allow": {
81
90
  "type": "array",
@@ -106,6 +115,33 @@
106
115
  "description": "Request that returns the stored object, e.g. 'GET /api/profile' (default: GET on the route path)."
107
116
  }
108
117
  }
118
+ },
119
+ "body_format": {
120
+ "enum": [
121
+ "json",
122
+ "form"
123
+ ],
124
+ "default": "json",
125
+ "description": "Body encoding. form sends application/x-www-form-urlencoded."
126
+ },
127
+ "csrf": {
128
+ "type": "object",
129
+ "required": [
130
+ "page",
131
+ "field"
132
+ ],
133
+ "additionalProperties": false,
134
+ "description": "Fetch this page before each write and send the token found in the named field.",
135
+ "properties": {
136
+ "page": {
137
+ "type": "string",
138
+ "pattern": "^/"
139
+ },
140
+ "field": {
141
+ "type": "string",
142
+ "minLength": 1
143
+ }
144
+ }
109
145
  }
110
146
  }
111
147
  }
@@ -10,6 +10,7 @@ RUN dotnet publish examples/member-portal/backend-dotnet/MemberPortal.Backend.cs
10
10
  FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS final
11
11
  WORKDIR /app
12
12
  COPY --from=build /app/publish .
13
- RUN mkdir -p /app/data
13
+ RUN mkdir -p /app/data && chown app:app /app/data
14
14
  EXPOSE 5001
15
+ USER app
15
16
  ENTRYPOINT ["dotnet", "MemberPortal.Backend.dll"]
@@ -9,5 +9,7 @@ RUN gradle -p examples/member-portal/backend-spring bootJar --no-daemon
9
9
  FROM eclipse-temurin:17-jre-alpine AS final
10
10
  WORKDIR /app
11
11
  COPY --from=build /home/gradle/src/examples/member-portal/backend-spring/build/libs/*.jar app.jar
12
+ RUN addgroup -S komagu && adduser -S -G komagu komagu
12
13
  EXPOSE 5002
14
+ USER komagu
13
15
  ENTRYPOINT ["java", "-jar", "app.jar"]
@@ -49,7 +49,7 @@ services:
49
49
  dockerfile: Dockerfile
50
50
  container_name: komagu-demo-frontend
51
51
  ports:
52
- - "3000:80"
52
+ - "3000:8080"
53
53
  depends_on:
54
54
  - backend-dotnet
55
55
 
@@ -5,7 +5,8 @@ RUN npm install
5
5
  COPY . .
6
6
  RUN npm run build
7
7
 
8
- FROM nginx:alpine
8
+ FROM nginxinc/nginx-unprivileged:alpine
9
9
  COPY --from=build /app/dist /usr/share/nginx/html
10
- EXPOSE 80
10
+ EXPOSE 8080
11
+ USER 101
11
12
  CMD ["nginx", "-g", "daemon off;"]