@hoanghainh/komagu 2.6.0 → 2.6.1

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 (32) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/examples/member-portal/backend-dotnet/Auth/DemoAuthenticationHandler.cs +75 -0
  3. package/examples/member-portal/backend-dotnet/Program.cs +1 -72
  4. package/examples/member-portal/backend-spring/src/main/kotlin/io/komagu/portal/Application.kt +4 -0
  5. package/examples/member-portal/backend-spring/src/main/kotlin/io/komagu/portal/controller/Controllers.kt +3 -0
  6. package/examples/member-portal/backend-spring/src/main/kotlin/io/komagu/portal/model/Entities.kt +4 -0
  7. package/examples/member-portal/tests/test_d6_demo_app.py +4 -4
  8. package/examples/member-portal/tests/test_d6_remediation_verification.py +6 -6
  9. package/operate/ai/client.py +5 -1
  10. package/operate/cli/__init__.py +1 -1
  11. package/operate/cross_user_tester/runner.py +4 -1
  12. package/operate/devsecops/sbom.py +12 -4
  13. package/operate/simulate/scenarios.py +11 -2
  14. package/operate/tests/test_audit_minimum_versions.py +89 -0
  15. package/operate/tests/test_cli.py +1 -1
  16. package/operate/tests/test_scheme_and_xml_guards.py +67 -0
  17. package/package.json +1 -1
  18. package/remediate/authz/dotnet/src/Komagu.Security.Authorization/LoggingSecurityAuditSink.cs +19 -3
  19. package/remediate/authz/golang/authz_test.go +5 -2
  20. package/remediate/authz/spring/src/main/kotlin/io/komagu/security/authorization/DefaultKomaguSecurityManager.kt +74 -78
  21. package/remediate/authz/spring/src/main/kotlin/io/komagu/security/spring/OwnershipAspect.kt +129 -158
  22. package/remediate/deps/patcher.py +2 -1
  23. package/remediate/edge/mock-backend/app.py +2 -1
  24. package/remediate/edge/tests/test_edge_security.py +5 -5
  25. package/remediate/ekyc/requirements.txt +1 -1
  26. package/remediate/identity/dotnet/src/Komagu.Security.Identity/OidcServiceCollectionExtensions.cs +19 -6
  27. package/remediate/identity/python/requirements.txt +2 -2
  28. package/remediate/identity/spring/src/main/kotlin/io/komagu/security/identity/KomaguIdentityAutoConfiguration.kt +31 -38
  29. package/remediate/legacy_shims/tests/test_legacy_shims.py +8 -8
  30. package/remediate/query/dotnet/src/Komagu.Security.Query/SafeSortAllowList.cs +5 -2
  31. package/remediate/query/spring/src/main/kotlin/io/komagu/security/query/SafeSortAllowList.kt +10 -11
  32. package/remediate/query/spring/src/main/kotlin/io/komagu/security/query/detekt/StringBuiltSqlQueryRule.kt +19 -24
@@ -186,7 +186,7 @@ class TestMockBackendTraffic(unittest.TestCase):
186
186
  "X-Custom-Header": "safe-value-unredacted",
187
187
  }
188
188
  )
189
- with urllib.request.urlopen(req, timeout=3) as resp:
189
+ with urllib.request.urlopen(req, timeout=3) as resp: # nosec B310 # local test server or KOMAGU_GATEWAY_URL under test
190
190
  self.assertEqual(resp.status, 200)
191
191
  data = json.loads(resp.read().decode("utf-8"))
192
192
 
@@ -388,7 +388,7 @@ class TestCorazaWafContainer(unittest.TestCase):
388
388
  data = form.encode("ascii")
389
389
  req = urllib.request.Request(f"http://127.0.0.1:{cls.port}{path}", data=data, headers=headers)
390
390
  try:
391
- with urllib.request.urlopen(req, timeout=5) as resp:
391
+ with urllib.request.urlopen(req, timeout=5) as resp: # nosec B310 # local test server or KOMAGU_GATEWAY_URL under test
392
392
  return resp.status
393
393
  except urllib.error.HTTPError as e:
394
394
  return e.code
@@ -505,7 +505,7 @@ class TestLiveEdgeSecurity(unittest.TestCase):
505
505
  def setUpClass(cls):
506
506
  try:
507
507
  req = urllib.request.Request(f"{cls.BASE_URL}/api/v1/health")
508
- with urllib.request.urlopen(req, timeout=1) as resp:
508
+ with urllib.request.urlopen(req, timeout=1) as resp: # nosec B310 # local test server or KOMAGU_GATEWAY_URL under test
509
509
  cls.live_available = True
510
510
  except Exception:
511
511
  cls.live_available = False
@@ -516,7 +516,7 @@ class TestLiveEdgeSecurity(unittest.TestCase):
516
516
 
517
517
  def test_live_legitimate_request_returns_200(self):
518
518
  req = urllib.request.Request(f"{self.BASE_URL}/api/v1/orders")
519
- with urllib.request.urlopen(req, timeout=3) as resp:
519
+ with urllib.request.urlopen(req, timeout=3) as resp: # nosec B310 # local test server or KOMAGU_GATEWAY_URL under test
520
520
  self.assertEqual(resp.status, 200)
521
521
  self.assertIn("X-Correlation-ID", resp.headers)
522
522
 
@@ -524,7 +524,7 @@ class TestLiveEdgeSecurity(unittest.TestCase):
524
524
  sqli_url = f"{self.BASE_URL}/api/v1/orders?id=1%27+OR+1%3D1--"
525
525
  req = urllib.request.Request(sqli_url)
526
526
  try:
527
- with urllib.request.urlopen(req, timeout=3) as resp:
527
+ with urllib.request.urlopen(req, timeout=3) as resp: # nosec B310 # local test server or KOMAGU_GATEWAY_URL under test
528
528
  self.fail(f"Expected 403 Forbidden but received {resp.status}")
529
529
  except urllib.error.HTTPError as e:
530
530
  self.assertEqual(e.code, 403)
@@ -1 +1 @@
1
- cryptography>=41.0.0
1
+ cryptography>=50.0.0
@@ -11,7 +11,7 @@ using Microsoft.IdentityModel.Tokens;
11
11
  /// <summary>
12
12
  /// Service collection extension methods for configuring Komagu OIDC / JWT authentication.
13
13
  /// </summary>
14
- public static class OidcServiceCollectionExtensions
14
+ public static partial class OidcServiceCollectionExtensions
15
15
  {
16
16
  /// <summary>
17
17
  /// Configures strict JWT Bearer token authentication conforming to RFC 9700 and Boilerplate 1 standards.
@@ -79,11 +79,14 @@ public static class OidcServiceCollectionExtensions
79
79
  var logger = context.HttpContext.RequestServices.GetService<ILoggerFactory>()
80
80
  ?.CreateLogger("Komagu.Security.Identity.Authentication");
81
81
 
82
- logger?.LogWarning(
83
- context.Exception,
84
- "[AUTH FAILURE] Token validation failed: {Message} | RemoteIP: {RemoteIp}",
85
- context.Exception.Message,
86
- context.HttpContext.Connection.RemoteIpAddress);
82
+ if (logger is not null)
83
+ {
84
+ LogTokenValidationFailed(
85
+ logger,
86
+ context.Exception,
87
+ context.Exception.Message,
88
+ context.HttpContext.Connection.RemoteIpAddress);
89
+ }
87
90
 
88
91
  return Task.CompletedTask;
89
92
  },
@@ -105,4 +108,14 @@ public static class OidcServiceCollectionExtensions
105
108
  };
106
109
  });
107
110
  }
111
+
112
+ [LoggerMessage(
113
+ EventId = 1,
114
+ Level = LogLevel.Warning,
115
+ Message = "[AUTH FAILURE] Token validation failed: {Message} | RemoteIP: {RemoteIp}")]
116
+ private static partial void LogTokenValidationFailed(
117
+ ILogger logger,
118
+ Exception exception,
119
+ string message,
120
+ System.Net.IPAddress? remoteIp);
108
121
  }
@@ -1,3 +1,3 @@
1
1
  # Python dependencies for Komagu OIDC Identity Token Validation (D2)
2
- pyjwt>=2.8.0
3
- cryptography>=42.0.0
2
+ pyjwt>=2.15.0
3
+ cryptography>=50.0.0
@@ -47,46 +47,12 @@ class KomaguIdentityAutoConfiguration {
47
47
  else -> ""
48
48
  }
49
49
 
50
- if (properties.audience.isBlank()) {
51
- throw IllegalStateException("komagu.security.oidc.audience must not be blank")
50
+ check(properties.audience.isNotBlank()) { "komagu.security.oidc.audience must not be blank" }
51
+ check(properties.clockSkewSeconds in 0L..120L) {
52
+ "komagu.security.oidc.clock-skew-seconds must be between 0 and 120 (got ${properties.clockSkewSeconds})"
52
53
  }
53
54
 
54
- if (properties.clockSkewSeconds < 0L || properties.clockSkewSeconds > 120L) {
55
- throw IllegalStateException(
56
- "komagu.security.oidc.clock-skew-seconds must be between 0 and 120 (got ${properties.clockSkewSeconds})"
57
- )
58
- }
59
-
60
- val jwkUri = properties.jwkSetUri?.trim()
61
- val decoder: NimbusJwtDecoder = when {
62
- !jwkUri.isNullOrBlank() -> {
63
- if (effectiveIssuer.isBlank()) {
64
- throw IllegalStateException("komagu.security.oidc.issuer must not be blank")
65
- }
66
- if (!properties.allowInsecureHttpJwks && !jwkUri.lowercase().startsWith("https://")) {
67
- throw IllegalStateException(
68
- "komagu.security.oidc.jwk-set-uri must use HTTPS ($jwkUri). " +
69
- "Set komagu.security.oidc.allow-insecure-http-jwks=true only for testing."
70
- )
71
- }
72
- NimbusJwtDecoder.withJwkSetUri(jwkUri).build()
73
- }
74
- effectiveIssuer.isNotBlank() -> {
75
- if (!properties.allowInsecureHttpJwks && !effectiveIssuer.lowercase().startsWith("https://")) {
76
- throw IllegalStateException(
77
- "OIDC issuer URI must use HTTPS ($effectiveIssuer). " +
78
- "Set komagu.security.oidc.allow-insecure-http-jwks=true only for testing."
79
- )
80
- }
81
- NimbusJwtDecoder.withIssuerLocation(effectiveIssuer).build()
82
- }
83
- else -> {
84
- throw IllegalStateException(
85
- "Either komagu.security.oidc.jwk-set-uri or komagu.security.oidc.issuer " +
86
- "(or spring.security.oauth2.resourceserver.jwt.issuer-uri) must be configured"
87
- )
88
- }
89
- }
55
+ val decoder = buildDecoder(properties, effectiveIssuer)
90
56
 
91
57
  val expirationValidator = KomaguExpirationValidator()
92
58
  val timestampValidator = JwtTimestampValidator(Duration.ofSeconds(properties.clockSkewSeconds))
@@ -106,6 +72,33 @@ class KomaguIdentityAutoConfiguration {
106
72
  return decoder
107
73
  }
108
74
 
75
+ private fun buildDecoder(properties: KomaguOidcProperties, effectiveIssuer: String): NimbusJwtDecoder {
76
+ val jwkUri = properties.jwkSetUri?.trim()
77
+ return when {
78
+ !jwkUri.isNullOrBlank() -> {
79
+ check(effectiveIssuer.isNotBlank()) { "komagu.security.oidc.issuer must not be blank" }
80
+ check(properties.allowInsecureHttpJwks || isHttps(jwkUri)) {
81
+ "komagu.security.oidc.jwk-set-uri must use HTTPS ($jwkUri). " +
82
+ "Set komagu.security.oidc.allow-insecure-http-jwks=true only for testing."
83
+ }
84
+ NimbusJwtDecoder.withJwkSetUri(jwkUri).build()
85
+ }
86
+ effectiveIssuer.isNotBlank() -> {
87
+ check(properties.allowInsecureHttpJwks || isHttps(effectiveIssuer)) {
88
+ "OIDC issuer URI must use HTTPS ($effectiveIssuer). " +
89
+ "Set komagu.security.oidc.allow-insecure-http-jwks=true only for testing."
90
+ }
91
+ NimbusJwtDecoder.withIssuerLocation(effectiveIssuer).build()
92
+ }
93
+ else -> error(
94
+ "Either komagu.security.oidc.jwk-set-uri or komagu.security.oidc.issuer " +
95
+ "(or spring.security.oauth2.resourceserver.jwt.issuer-uri) must be configured"
96
+ )
97
+ }
98
+ }
99
+
100
+ private fun isHttps(uri: String): Boolean = uri.lowercase().startsWith("https://")
101
+
109
102
  @Bean
110
103
  @ConditionalOnMissingBean(SecurityFilterChain::class)
111
104
  fun komaguSecurityFilterChain(
@@ -222,23 +222,23 @@ public class OwnershipEnforcerBehaviouralTest {
222
222
  def test_php_shim_behavioural_execution(self) -> None:
223
223
  """Verify PHP shim behavioral normalization and denial with SQLite in-memory PDO."""
224
224
  php_file = SHIMS_ROOT / "php" / "komagu_authz_shim.php"
225
- php_test_script = f"""
226
- require_once '{php_file}';
225
+ php_test_script = """
226
+ require_once '__PHP_FILE__';
227
227
 
228
228
  // 1. Test komagu_normalize_id
229
229
  assert(komagu_normalize_id(1) === 1);
230
230
  assert(komagu_normalize_id('42') === 42);
231
231
 
232
232
  $invalidIds = [null, '', false, true, 0, '0', -1, '-5', '007', '1e2', '0x5', '1.5'];
233
- foreach ($invalidIds as $inv) {{
234
- try {{
233
+ foreach ($invalidIds as $inv) {
234
+ try {
235
235
  komagu_normalize_id($inv);
236
236
  echo "FAIL: Expected exception for " . var_export($inv, true) . "\\n";
237
237
  exit(1);
238
- }} catch (InvalidArgumentException $e) {{
238
+ } catch (InvalidArgumentException $e) {
239
239
  // Expected
240
- }}
241
- }}
240
+ }
241
+ }
242
242
 
243
243
  // 2. Test komagu_check_ownership with SQLite in-memory PDO
244
244
  $pdo = new PDO('sqlite::memory:');
@@ -258,7 +258,7 @@ public class OwnershipEnforcerBehaviouralTest {
258
258
  assert(komagu_check_ownership($pdo, 'orders', 'id', '', 'user_id', 100, $allowed) === false);
259
259
 
260
260
  echo "ALL_PHP_BEHAVIOURAL_TESTS_PASSED\\n";
261
- """
261
+ """.replace("__PHP_FILE__", str(php_file)) # nosec B608 # PHP test script with a fixed repo path, not a query
262
262
  proc = subprocess.run(
263
263
  ["php", "-r", php_test_script],
264
264
  capture_output=True,
@@ -16,9 +16,9 @@ public enum SortDirection
16
16
  /// Strict allow-list validator for dynamic column names and sort clauses.
17
17
  /// Prevents SQL injection via dynamic ORDER BY clauses.
18
18
  /// </summary>
19
- public sealed class SafeSortAllowList
19
+ public sealed partial class SafeSortAllowList
20
20
  {
21
- private static readonly Regex SafeIdentifierRegex = new("^[a-zA-Z_][a-zA-Z0-9_]*$", RegexOptions.Compiled);
21
+ private static readonly Regex SafeIdentifierRegex = SafeIdentifier();
22
22
  private readonly HashSet<string> _allowedColumns;
23
23
 
24
24
  public SafeSortAllowList(IEnumerable<string> allowedColumns)
@@ -73,4 +73,7 @@ public sealed class SafeSortAllowList
73
73
  var dirString = direction == SortDirection.Descending ? "DESC" : "ASC";
74
74
  return $"{validColumn} {dirString}";
75
75
  }
76
+
77
+ [GeneratedRegex("^[a-zA-Z_][a-zA-Z0-9_]*$")]
78
+ private static partial Regex SafeIdentifier();
76
79
  }
@@ -25,18 +25,17 @@ class SafeSortAllowList(allowedColumns: Collection<String>) {
25
25
  * Throws SecurityException on validation failure.
26
26
  */
27
27
  fun validateColumnName(columnName: String?): String {
28
- if (columnName.isNullOrBlank()) {
29
- throw SecurityException("Sort column name cannot be null or blank")
30
- }
31
-
32
- val trimmed = columnName.trim()
33
-
34
- if (!safeIdentifierRegex.matches(trimmed)) {
35
- throw SecurityException("Potential SQL injection detected in sort column: '$columnName'. Illegal characters detected.")
36
- }
28
+ val trimmed = columnName?.trim().orEmpty()
29
+ val isIdentifier = trimmed.isNotEmpty() && safeIdentifierRegex.matches(trimmed)
30
+ val canonical = if (isIdentifier) allowedMap[trimmed.lowercase()] else null
31
+ return canonical ?: throw SecurityException(rejectionReason(columnName, trimmed))
32
+ }
37
33
 
38
- return allowedMap[trimmed.lowercase()]
39
- ?: throw SecurityException("Unauthorized sort column requested: '$columnName'. Column is not in allowed list.")
34
+ private fun rejectionReason(columnName: String?, trimmed: String): String = when {
35
+ trimmed.isEmpty() -> "Sort column name cannot be null or blank"
36
+ !safeIdentifierRegex.matches(trimmed) ->
37
+ "Potential SQL injection detected in sort column: '$columnName'. Illegal characters detected."
38
+ else -> "Unauthorized sort column requested: '$columnName'. Column is not in allowed list."
40
39
  }
41
40
 
42
41
  /**
@@ -1,6 +1,12 @@
1
1
  package io.komagu.security.query.detekt
2
2
 
3
- import io.gitlab.arturbosch.detekt.api.*
3
+ import io.gitlab.arturbosch.detekt.api.CodeSmell
4
+ import io.gitlab.arturbosch.detekt.api.Config
5
+ import io.gitlab.arturbosch.detekt.api.Debt
6
+ import io.gitlab.arturbosch.detekt.api.Entity
7
+ import io.gitlab.arturbosch.detekt.api.Issue
8
+ import io.gitlab.arturbosch.detekt.api.Rule
9
+ import io.gitlab.arturbosch.detekt.api.Severity
4
10
  import org.jetbrains.kotlin.lexer.KtTokens
5
11
  import org.jetbrains.kotlin.psi.KtBinaryExpression
6
12
  import org.jetbrains.kotlin.psi.KtCallExpression
@@ -33,37 +39,26 @@ class StringBuiltSqlQueryRule(config: Config = Config.empty) : Rule(config) {
33
39
  override fun visitCallExpression(expression: KtCallExpression) {
34
40
  super.visitCallExpression(expression)
35
41
 
36
- val callee = expression.calleeExpression?.text ?: return
37
- if (!targetSqlMethods.contains(callee)) {
42
+ val callee = expression.calleeExpression?.text
43
+ val firstArg = expression.valueArguments.firstOrNull()?.getArgumentExpression()
44
+ if (callee == null || callee !in targetSqlMethods || firstArg == null) {
38
45
  return
39
46
  }
40
47
 
41
- val arguments = expression.valueArguments
42
- if (arguments.isEmpty()) {
43
- return
44
- }
45
-
46
- val firstArg = arguments[0].getArgumentExpression() ?: return
47
-
48
- // 1. String template with variable interpolation: "SELECT * FROM orders WHERE id = $orderId"
49
- if (firstArg is KtStringTemplateExpression && firstArg.hasInterpolation()) {
50
- report(
51
- CodeSmell(
52
- issue = issue,
53
- entity = Entity.from(firstArg),
54
- message = "SQL/JPQL query in '$callee' uses string template interpolation. All parameters must be passed as parameterized bindings."
55
- )
56
- )
57
- return
48
+ val construction = when {
49
+ // 1. String template with variable interpolation: "SELECT * FROM orders WHERE id = $orderId"
50
+ firstArg is KtStringTemplateExpression && firstArg.hasInterpolation() -> "uses string template interpolation"
51
+ // 2. Binary string concatenation: "SELECT * FROM orders WHERE id = " + orderId
52
+ firstArg is KtBinaryExpression && firstArg.operationToken == KtTokens.PLUS ->
53
+ "is built using binary string concatenation (+)"
54
+ else -> null
58
55
  }
59
-
60
- // 2. Binary string concatenation: "SELECT * FROM orders WHERE id = " + orderId
61
- if (firstArg is KtBinaryExpression && firstArg.operationToken == KtTokens.PLUS) {
56
+ if (construction != null) {
62
57
  report(
63
58
  CodeSmell(
64
59
  issue = issue,
65
60
  entity = Entity.from(firstArg),
66
- message = "SQL/JPQL query in '$callee' is built using binary string concatenation (+). All parameters must be passed as parameterized bindings."
61
+ message = "SQL/JPQL query in '$callee' $construction. All parameters must be passed as parameterized bindings."
67
62
  )
68
63
  )
69
64
  }