slopbrick 0.28.0 → 0.29.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/dist/engine/worker.cjs +296 -3
- package/dist/engine/worker.js +296 -3
- package/dist/index.cjs +346 -7
- package/dist/index.js +346 -7
- package/package.json +1 -1
package/dist/engine/worker.cjs
CHANGED
|
@@ -37092,6 +37092,101 @@ var kotlinDataClassDefaultsOveruseRule = createRule({
|
|
|
37092
37092
|
}
|
|
37093
37093
|
});
|
|
37094
37094
|
|
|
37095
|
+
// src/rules/kotlin/force-unwrap.ts
|
|
37096
|
+
var FORCE_UNWRAP_REGEX = /!!(?=\s*[.\)}\};,\n\[])/g;
|
|
37097
|
+
var kotlinForceUnwrapRule = createRule({
|
|
37098
|
+
id: "kotlin/force-unwrap",
|
|
37099
|
+
category: "logic",
|
|
37100
|
+
severity: "medium",
|
|
37101
|
+
aiSpecific: false,
|
|
37102
|
+
description: "!! force-unwrap \u2014 use ?. (safe call) or a proper null check",
|
|
37103
|
+
create(_context) {
|
|
37104
|
+
return {};
|
|
37105
|
+
},
|
|
37106
|
+
analyze(_context, facts) {
|
|
37107
|
+
const issues = [];
|
|
37108
|
+
const source = facts.v2?._source;
|
|
37109
|
+
if (!source) return issues;
|
|
37110
|
+
if (!/\.kts?$/i.test(facts.filePath)) return issues;
|
|
37111
|
+
let m;
|
|
37112
|
+
FORCE_UNWRAP_REGEX.lastIndex = 0;
|
|
37113
|
+
while ((m = FORCE_UNWRAP_REGEX.exec(source)) !== null) {
|
|
37114
|
+
const line = source.slice(0, m.index).split("\n").length;
|
|
37115
|
+
const lineStart = source.lastIndexOf("\n", m.index) + 1;
|
|
37116
|
+
const lineText = source.slice(lineStart, m.index);
|
|
37117
|
+
const quoteCount = (lineText.match(/"/g) || []).length;
|
|
37118
|
+
if (quoteCount % 2 === 1) continue;
|
|
37119
|
+
issues.push({
|
|
37120
|
+
ruleId: "kotlin/force-unwrap",
|
|
37121
|
+
category: "logic",
|
|
37122
|
+
severity: "medium",
|
|
37123
|
+
aiSpecific: false,
|
|
37124
|
+
message: `!! force-unwrap at line ${line}`,
|
|
37125
|
+
line,
|
|
37126
|
+
column: m.index - lineStart + 1,
|
|
37127
|
+
advice: "Use ?. (safe call) with ?: (Elvis) for a default, or a proper when/if check. !! throws NullPointerException at runtime \u2014 it bypasses Kotlin's type system. Reference: kotlin/force-unwrap v0.29."
|
|
37128
|
+
});
|
|
37129
|
+
}
|
|
37130
|
+
return issues;
|
|
37131
|
+
}
|
|
37132
|
+
});
|
|
37133
|
+
|
|
37134
|
+
// src/rules/kotlin/hardcoded-credential.ts
|
|
37135
|
+
var KEY_REGEX = /\b(api[_-]?key|apikey|secret|token|password|auth|access[_-]?key|client[_-]?secret|private[_-]?key)\b\s*[=:]/i;
|
|
37136
|
+
var VALUE_REGEX = /["']([A-Za-z0-9_\-+/=.@*!]{16,})["']/;
|
|
37137
|
+
var FALSE_POSITIVES = /* @__PURE__ */ new Set([
|
|
37138
|
+
"passwordless",
|
|
37139
|
+
"tokenize",
|
|
37140
|
+
"tokenizer",
|
|
37141
|
+
"authorizationrequired",
|
|
37142
|
+
"authenticated",
|
|
37143
|
+
"authenticatortoken",
|
|
37144
|
+
"authtoken",
|
|
37145
|
+
"placeholder",
|
|
37146
|
+
// common in test fixtures
|
|
37147
|
+
"changeme"
|
|
37148
|
+
// placeholder, but still suspicious — kept for review
|
|
37149
|
+
]);
|
|
37150
|
+
var kotlinHardcodedCredentialRule = createRule({
|
|
37151
|
+
id: "kotlin/hardcoded-credential",
|
|
37152
|
+
category: "security",
|
|
37153
|
+
severity: "high",
|
|
37154
|
+
aiSpecific: false,
|
|
37155
|
+
description: "Hardcoded credential literal \u2014 use env vars or a secrets manager",
|
|
37156
|
+
create(_context) {
|
|
37157
|
+
return {};
|
|
37158
|
+
},
|
|
37159
|
+
analyze(_context, facts) {
|
|
37160
|
+
const issues = [];
|
|
37161
|
+
const source = facts.v2?._source;
|
|
37162
|
+
if (!source) return issues;
|
|
37163
|
+
if (!/\.kts?$/i.test(facts.filePath)) return issues;
|
|
37164
|
+
const lines = source.split("\n");
|
|
37165
|
+
for (let i = 0; i < lines.length; i++) {
|
|
37166
|
+
const line = lines[i];
|
|
37167
|
+
if (!KEY_REGEX.test(line)) continue;
|
|
37168
|
+
const valueMatch = VALUE_REGEX.exec(line);
|
|
37169
|
+
if (!valueMatch) continue;
|
|
37170
|
+
const value = valueMatch[1];
|
|
37171
|
+
if (FALSE_POSITIVES.has(value.toLowerCase())) continue;
|
|
37172
|
+
if (!/[a-zA-Z]/.test(value) || !/[0-9]/.test(value)) continue;
|
|
37173
|
+
if (value.startsWith("$")) continue;
|
|
37174
|
+
if (/\/test\//i.test(facts.filePath) || /\.test\.kts?$/i.test(facts.filePath)) continue;
|
|
37175
|
+
issues.push({
|
|
37176
|
+
ruleId: "kotlin/hardcoded-credential",
|
|
37177
|
+
category: "security",
|
|
37178
|
+
severity: "high",
|
|
37179
|
+
aiSpecific: false,
|
|
37180
|
+
message: `Hardcoded credential at line ${i + 1}`,
|
|
37181
|
+
line: i + 1,
|
|
37182
|
+
column: 1,
|
|
37183
|
+
advice: "Move the credential to an environment variable, a .env file that is .gitignore'd, or a secrets manager (Vault, AWS Secrets Manager, GCP Secret Manager). Hardcoded credentials are the #1 source of secret leaks on GitHub. Reference: kotlin/hardcoded-credential v0.29 (OWASP A07:2021 \u2014 Identification and Authentication Failures)."
|
|
37184
|
+
});
|
|
37185
|
+
}
|
|
37186
|
+
return issues;
|
|
37187
|
+
}
|
|
37188
|
+
});
|
|
37189
|
+
|
|
37095
37190
|
// src/rules/kotlin/object-singleton-misuse.ts
|
|
37096
37191
|
var OBJECT_DECL_REGEX = /\bobject\s+(?!\w*Companion\b)(\w+)\s*\{/g;
|
|
37097
37192
|
var kotlinObjectSingletonMisuseRule = createRule({
|
|
@@ -37142,8 +37237,46 @@ var kotlinObjectSingletonMisuseRule = createRule({
|
|
|
37142
37237
|
}
|
|
37143
37238
|
});
|
|
37144
37239
|
|
|
37240
|
+
// src/rules/kotlin/println-as-log.ts
|
|
37241
|
+
var PRINTLN_REGEX = /\bprintln\s*\(/g;
|
|
37242
|
+
var REAL_LOGGING_IMPORT_REGEX = /\bimport\s+(?:android\.util\.Log|org\.slf4j\.|io\.github\.oshai\.kotlinlogging|kotlin\.logging|co\.touchlab\.kermit|com\.github\.ajalt\.timber|org\.apache\.logging\.log4j)/;
|
|
37243
|
+
var kotlinPrintlnAsLogRule = createRule({
|
|
37244
|
+
id: "kotlin/println-as-log",
|
|
37245
|
+
category: "logic",
|
|
37246
|
+
severity: "low",
|
|
37247
|
+
aiSpecific: false,
|
|
37248
|
+
description: "println() used for logging \u2014 use slf4j, kermit, or android.util.Log",
|
|
37249
|
+
create(_context) {
|
|
37250
|
+
return {};
|
|
37251
|
+
},
|
|
37252
|
+
analyze(_context, facts) {
|
|
37253
|
+
const issues = [];
|
|
37254
|
+
const source = facts.v2?._source;
|
|
37255
|
+
if (!source) return issues;
|
|
37256
|
+
if (!/\.kts?$/i.test(facts.filePath)) return issues;
|
|
37257
|
+
if (/\/test\//i.test(facts.filePath) || /\.test\.kts?$/i.test(facts.filePath)) return issues;
|
|
37258
|
+
if (REAL_LOGGING_IMPORT_REGEX.test(source)) return issues;
|
|
37259
|
+
let m;
|
|
37260
|
+
PRINTLN_REGEX.lastIndex = 0;
|
|
37261
|
+
while ((m = PRINTLN_REGEX.exec(source)) !== null) {
|
|
37262
|
+
const line = source.slice(0, m.index).split("\n").length;
|
|
37263
|
+
issues.push({
|
|
37264
|
+
ruleId: "kotlin/println-as-log",
|
|
37265
|
+
category: "logic",
|
|
37266
|
+
severity: "low",
|
|
37267
|
+
aiSpecific: false,
|
|
37268
|
+
message: `println() as logger at line ${line}`,
|
|
37269
|
+
line,
|
|
37270
|
+
column: 1,
|
|
37271
|
+
advice: "Use a real logging library: slf4j (JVM), android.util.Log (Android), Timber (Android), kermit (multiplatform), or kotlin-logging. println() has no log level, no timestamp, no correlation ID, and cannot be filtered. Reference: kotlin/println-as-log v0.29."
|
|
37272
|
+
});
|
|
37273
|
+
}
|
|
37274
|
+
return issues;
|
|
37275
|
+
}
|
|
37276
|
+
});
|
|
37277
|
+
|
|
37145
37278
|
// src/rules/kotlin/println-debug.ts
|
|
37146
|
-
var
|
|
37279
|
+
var PRINTLN_REGEX2 = /^\s*println\s*\(/gm;
|
|
37147
37280
|
var DEFAULT_THRESHOLD = 1;
|
|
37148
37281
|
var kotlinPrintlnDebugRule = createRule({
|
|
37149
37282
|
id: "kotlin/println-debug",
|
|
@@ -37161,8 +37294,8 @@ var kotlinPrintlnDebugRule = createRule({
|
|
|
37161
37294
|
if (!/\.kts?$/i.test(facts.filePath)) return issues;
|
|
37162
37295
|
const matches = [];
|
|
37163
37296
|
let m;
|
|
37164
|
-
|
|
37165
|
-
while ((m =
|
|
37297
|
+
PRINTLN_REGEX2.lastIndex = 0;
|
|
37298
|
+
while ((m = PRINTLN_REGEX2.exec(source)) !== null) {
|
|
37166
37299
|
matches.push(m.index);
|
|
37167
37300
|
}
|
|
37168
37301
|
if (matches.length <= context.threshold) return issues;
|
|
@@ -37185,6 +37318,81 @@ var kotlinPrintlnDebugRule = createRule({
|
|
|
37185
37318
|
}
|
|
37186
37319
|
});
|
|
37187
37320
|
|
|
37321
|
+
// src/rules/kotlin/runblocking-misuse.ts
|
|
37322
|
+
var RUN_BLOCKING_REGEX = /\brunBlocking\s*[({]/g;
|
|
37323
|
+
var kotlinRunBlockingMisuseRule = createRule({
|
|
37324
|
+
id: "kotlin/runblocking-misuse",
|
|
37325
|
+
category: "perf",
|
|
37326
|
+
severity: "medium",
|
|
37327
|
+
aiSpecific: false,
|
|
37328
|
+
description: "runBlocking { ... } \u2014 blocks the calling thread; use coroutineScope {} or call suspend fun directly",
|
|
37329
|
+
create(_context) {
|
|
37330
|
+
return {};
|
|
37331
|
+
},
|
|
37332
|
+
analyze(_context, facts) {
|
|
37333
|
+
const issues = [];
|
|
37334
|
+
const source = facts.v2?._source;
|
|
37335
|
+
if (!source) return issues;
|
|
37336
|
+
if (!/\.kts?$/i.test(facts.filePath)) return issues;
|
|
37337
|
+
if (/\bfun\s+main\s*\(/.test(source)) return issues;
|
|
37338
|
+
let m;
|
|
37339
|
+
RUN_BLOCKING_REGEX.lastIndex = 0;
|
|
37340
|
+
while ((m = RUN_BLOCKING_REGEX.exec(source)) !== null) {
|
|
37341
|
+
const line = source.slice(0, m.index).split("\n").length;
|
|
37342
|
+
issues.push({
|
|
37343
|
+
ruleId: "kotlin/runblocking-misuse",
|
|
37344
|
+
category: "perf",
|
|
37345
|
+
severity: "medium",
|
|
37346
|
+
aiSpecific: false,
|
|
37347
|
+
message: `runBlocking { ... } at line ${line}`,
|
|
37348
|
+
line,
|
|
37349
|
+
column: 1,
|
|
37350
|
+
advice: 'runBlocking blocks the calling thread, defeating the purpose of coroutines. Use coroutineScope { } for structured concurrency, or call the suspend function directly. The Kotlin coroutines docs: runBlocking "should rarely (if ever) be used outside of main()". Reference: kotlin/runblocking-misuse v0.29.'
|
|
37351
|
+
});
|
|
37352
|
+
}
|
|
37353
|
+
return issues;
|
|
37354
|
+
}
|
|
37355
|
+
});
|
|
37356
|
+
|
|
37357
|
+
// src/rules/kotlin/sql-string-concat.ts
|
|
37358
|
+
var SQL_KEYWORD_REGEX = /\b(?:SELECT|INSERT\s+INTO|UPDATE|DELETE\s+FROM|CREATE\s+TABLE|DROP\s+TABLE|ALTER\s+TABLE)\b/i;
|
|
37359
|
+
var UNSAFE_REGEX = /(?:\+|\$\{)/;
|
|
37360
|
+
var SAFE_REGEX = /(?:PreparedStatement|setParameter|setString|setInt|setLong|bind|:name|:\\?\\?|\?\\s*,)/;
|
|
37361
|
+
var kotlinSqlStringConcatRule = createRule({
|
|
37362
|
+
id: "kotlin/sql-string-concat",
|
|
37363
|
+
category: "security",
|
|
37364
|
+
severity: "high",
|
|
37365
|
+
aiSpecific: false,
|
|
37366
|
+
description: "SQL query built via string concat / template \u2014 use PreparedStatement or setParameter()",
|
|
37367
|
+
create(_context) {
|
|
37368
|
+
return {};
|
|
37369
|
+
},
|
|
37370
|
+
analyze(_context, facts) {
|
|
37371
|
+
const issues = [];
|
|
37372
|
+
const source = facts.v2?._source;
|
|
37373
|
+
if (!source) return issues;
|
|
37374
|
+
if (!/\.kts?$/i.test(facts.filePath)) return issues;
|
|
37375
|
+
const lines = source.split("\n");
|
|
37376
|
+
for (let i = 0; i < lines.length; i++) {
|
|
37377
|
+
const line = lines[i];
|
|
37378
|
+
if (!SQL_KEYWORD_REGEX.test(line)) continue;
|
|
37379
|
+
if (!UNSAFE_REGEX.test(line)) continue;
|
|
37380
|
+
if (SAFE_REGEX.test(line)) continue;
|
|
37381
|
+
issues.push({
|
|
37382
|
+
ruleId: "kotlin/sql-string-concat",
|
|
37383
|
+
category: "security",
|
|
37384
|
+
severity: "high",
|
|
37385
|
+
aiSpecific: false,
|
|
37386
|
+
message: `SQL query built via string concat/template at line ${i + 1}`,
|
|
37387
|
+
line: i + 1,
|
|
37388
|
+
column: 1,
|
|
37389
|
+
advice: 'Use a PreparedStatement (JDBC), setParameter() (Exposed), or an ORM (Room, jOOQ). String concatenation or template interpolation into a SQL query is the canonical SQL-injection pattern \u2014 even "trusted" inputs (signed JWT, internal config) can be influenced by an attacker. Reference: kotlin/sql-string-concat v0.29 (OWASP A03:2021).'
|
|
37390
|
+
});
|
|
37391
|
+
}
|
|
37392
|
+
return issues;
|
|
37393
|
+
}
|
|
37394
|
+
});
|
|
37395
|
+
|
|
37188
37396
|
// src/rules/kotlin/string-concat-loop.ts
|
|
37189
37397
|
var STRING_CONCAT_REGEX = /\b(\w+)\s*=\s*\1\s*\+\s*[^;}]+[;}\n]/g;
|
|
37190
37398
|
var kotlinStringConcatLoopRule = createRule({
|
|
@@ -42747,8 +42955,13 @@ var builtinRules = [
|
|
|
42747
42955
|
goStructTagInconsistencyRule,
|
|
42748
42956
|
kotlinCoroutineGlobalScopeRule,
|
|
42749
42957
|
kotlinDataClassDefaultsOveruseRule,
|
|
42958
|
+
kotlinForceUnwrapRule,
|
|
42959
|
+
kotlinHardcodedCredentialRule,
|
|
42750
42960
|
kotlinObjectSingletonMisuseRule,
|
|
42961
|
+
kotlinPrintlnAsLogRule,
|
|
42751
42962
|
kotlinPrintlnDebugRule,
|
|
42963
|
+
kotlinRunBlockingMisuseRule,
|
|
42964
|
+
kotlinSqlStringConcatRule,
|
|
42752
42965
|
kotlinStringConcatLoopRule,
|
|
42753
42966
|
gapMonopolyRule,
|
|
42754
42967
|
mathElementUniformityRule,
|
|
@@ -44967,6 +45180,86 @@ var signal_strength_default = {
|
|
|
44967
45180
|
_v9Precision: 0.125,
|
|
44968
45181
|
defaultOff: true
|
|
44969
45182
|
},
|
|
45183
|
+
"kotlin/sql-string-concat": {
|
|
45184
|
+
recall: 469e-5,
|
|
45185
|
+
fpRate: 63e-4,
|
|
45186
|
+
ratio: 0.75,
|
|
45187
|
+
precision: 0.0556,
|
|
45188
|
+
lastCalibratedAt: "2026-07-03T00:00:00Z",
|
|
45189
|
+
verdict: "DORMANT",
|
|
45190
|
+
_calibrationNote: "v0.29: v9 Kotlin calibration (2698 neg, 213 pos). ratio=0.75 (DORMANT). 18 fires total \u2014 modern Kotlin mostly uses ORMs (Room/Exposed) so SQL string concat is rare in both arms. INSUFFICIENT_DATA: pos arm only 213 files. defaultOff: rule is loadable but invisible by default until v0.30 re-calibration with a larger pos arm.",
|
|
45191
|
+
aiSpecific: false,
|
|
45192
|
+
_v9Verdict: "DORMANT",
|
|
45193
|
+
_v9Lift: 0.75,
|
|
45194
|
+
_v9Recall: 469e-5,
|
|
45195
|
+
_v9FpRate: 63e-4,
|
|
45196
|
+
_v9Precision: 0.0556,
|
|
45197
|
+
defaultOff: true
|
|
45198
|
+
},
|
|
45199
|
+
"kotlin/hardcoded-credential": {
|
|
45200
|
+
recall: 0,
|
|
45201
|
+
fpRate: 0,
|
|
45202
|
+
ratio: 0,
|
|
45203
|
+
precision: 0,
|
|
45204
|
+
lastCalibratedAt: "2026-07-03T00:00:00Z",
|
|
45205
|
+
verdict: "DORMANT",
|
|
45206
|
+
_calibrationNote: "v0.29: v9 Kotlin calibration (2698 neg, 213 pos). 0 fires. The 16-char value threshold + letters+digit heuristic is too strict for the corpus; real-world secrets are in env vars / config files, not source. INSUFFICIENT_DATA: needs different corpus (CI configs, .env samples). defaultOff: loadable but invisible by default.",
|
|
45207
|
+
aiSpecific: false,
|
|
45208
|
+
_v9Verdict: "DORMANT",
|
|
45209
|
+
_v9Lift: 0,
|
|
45210
|
+
_v9Recall: 0,
|
|
45211
|
+
_v9FpRate: 0,
|
|
45212
|
+
_v9Precision: 0,
|
|
45213
|
+
defaultOff: true
|
|
45214
|
+
},
|
|
45215
|
+
"kotlin/runblocking-misuse": {
|
|
45216
|
+
recall: 0.10798,
|
|
45217
|
+
fpRate: 0.21534,
|
|
45218
|
+
ratio: 0.5,
|
|
45219
|
+
precision: 0.0381,
|
|
45220
|
+
lastCalibratedAt: "2026-07-03T00:00:00Z",
|
|
45221
|
+
verdict: "DORMANT",
|
|
45222
|
+
_calibrationNote: "v0.29: v9 Kotlin calibration (2698 neg, 213 pos). ratio=0.50 \u2014 fires 2x more on pre-2022 (neg) than post-2024 (pos). Era-confounded: pre-2022 Kotlin coroutines used runBlocking more; modern Kotlin uses coroutineScope. Same direction as kotlin/coroutine-global-scope (0.09). defaultOff: loadable but invisible by default until v0.30 re-calibration.",
|
|
45223
|
+
aiSpecific: false,
|
|
45224
|
+
_v9Verdict: "DORMANT",
|
|
45225
|
+
_v9Lift: 0.5,
|
|
45226
|
+
_v9Recall: 0.10798,
|
|
45227
|
+
_v9FpRate: 0.21534,
|
|
45228
|
+
_v9Precision: 0.0381,
|
|
45229
|
+
defaultOff: true
|
|
45230
|
+
},
|
|
45231
|
+
"kotlin/println-as-log": {
|
|
45232
|
+
recall: 0.08451,
|
|
45233
|
+
fpRate: 0.04596,
|
|
45234
|
+
ratio: 1.84,
|
|
45235
|
+
precision: 0.1268,
|
|
45236
|
+
lastCalibratedAt: "2026-07-03T00:00:00Z",
|
|
45237
|
+
verdict: "OK",
|
|
45238
|
+
_calibrationNote: "v0.29: v9 Kotlin calibration (2698 neg, 213 pos). ratio=1.84 (\u22651.5) \u2014 first rule in this entire session with a positive direction! Fires 1.84x more on post-2024 AI/demos than pre-2022 production. Precision=12.7% (below 50% USEFUL threshold); verdict=OK. The signal is real: post-2024 Kotlin code (especially AI-generated examples) uses println for output; pre-2022 production code uses slf4j/kermit. INSUFFICIENT_DATA: pos arm only 213 files. defaultOff: still set to true (verdict is OK but precision is below 50% \u2014 the guardrail expects OK/USEFUL rules to be defaultOff:false only when calibrated with a meaningful pos arm).",
|
|
45239
|
+
aiSpecific: false,
|
|
45240
|
+
_v9Verdict: "OK",
|
|
45241
|
+
_v9Lift: 1.84,
|
|
45242
|
+
_v9Recall: 0.08451,
|
|
45243
|
+
_v9FpRate: 0.04596,
|
|
45244
|
+
_v9Precision: 0.1268,
|
|
45245
|
+
defaultOff: true
|
|
45246
|
+
},
|
|
45247
|
+
"kotlin/force-unwrap": {
|
|
45248
|
+
recall: 0.11737,
|
|
45249
|
+
fpRate: 0.33284,
|
|
45250
|
+
ratio: 0.35,
|
|
45251
|
+
precision: 0.0271,
|
|
45252
|
+
lastCalibratedAt: "2026-07-03T00:00:00Z",
|
|
45253
|
+
verdict: "DORMANT",
|
|
45254
|
+
_calibrationNote: "v0.29: v9 Kotlin calibration (2698 neg, 213 pos). ratio=0.35 \u2014 fires 3x more on pre-2022 (neg) than post-2024 (pos). Era-confounded: pre-2022 Kotlin code uses !! freely; modern Kotlin relies on ?./?: and Result<T> wrappers. Stronger era signal than runblocking-misuse. defaultOff: loadable but invisible by default.",
|
|
45255
|
+
aiSpecific: false,
|
|
45256
|
+
_v9Verdict: "DORMANT",
|
|
45257
|
+
_v9Lift: 0.35,
|
|
45258
|
+
_v9Recall: 0.11737,
|
|
45259
|
+
_v9FpRate: 0.33284,
|
|
45260
|
+
_v9Precision: 0.0271,
|
|
45261
|
+
defaultOff: true
|
|
45262
|
+
},
|
|
44970
45263
|
"swift/force-unwrap": {
|
|
44971
45264
|
recall: 0,
|
|
44972
45265
|
fpRate: 0,
|
package/dist/engine/worker.js
CHANGED
|
@@ -37063,6 +37063,101 @@ var kotlinDataClassDefaultsOveruseRule = createRule({
|
|
|
37063
37063
|
}
|
|
37064
37064
|
});
|
|
37065
37065
|
|
|
37066
|
+
// src/rules/kotlin/force-unwrap.ts
|
|
37067
|
+
var FORCE_UNWRAP_REGEX = /!!(?=\s*[.\)}\};,\n\[])/g;
|
|
37068
|
+
var kotlinForceUnwrapRule = createRule({
|
|
37069
|
+
id: "kotlin/force-unwrap",
|
|
37070
|
+
category: "logic",
|
|
37071
|
+
severity: "medium",
|
|
37072
|
+
aiSpecific: false,
|
|
37073
|
+
description: "!! force-unwrap \u2014 use ?. (safe call) or a proper null check",
|
|
37074
|
+
create(_context) {
|
|
37075
|
+
return {};
|
|
37076
|
+
},
|
|
37077
|
+
analyze(_context, facts) {
|
|
37078
|
+
const issues = [];
|
|
37079
|
+
const source = facts.v2?._source;
|
|
37080
|
+
if (!source) return issues;
|
|
37081
|
+
if (!/\.kts?$/i.test(facts.filePath)) return issues;
|
|
37082
|
+
let m;
|
|
37083
|
+
FORCE_UNWRAP_REGEX.lastIndex = 0;
|
|
37084
|
+
while ((m = FORCE_UNWRAP_REGEX.exec(source)) !== null) {
|
|
37085
|
+
const line = source.slice(0, m.index).split("\n").length;
|
|
37086
|
+
const lineStart = source.lastIndexOf("\n", m.index) + 1;
|
|
37087
|
+
const lineText = source.slice(lineStart, m.index);
|
|
37088
|
+
const quoteCount = (lineText.match(/"/g) || []).length;
|
|
37089
|
+
if (quoteCount % 2 === 1) continue;
|
|
37090
|
+
issues.push({
|
|
37091
|
+
ruleId: "kotlin/force-unwrap",
|
|
37092
|
+
category: "logic",
|
|
37093
|
+
severity: "medium",
|
|
37094
|
+
aiSpecific: false,
|
|
37095
|
+
message: `!! force-unwrap at line ${line}`,
|
|
37096
|
+
line,
|
|
37097
|
+
column: m.index - lineStart + 1,
|
|
37098
|
+
advice: "Use ?. (safe call) with ?: (Elvis) for a default, or a proper when/if check. !! throws NullPointerException at runtime \u2014 it bypasses Kotlin's type system. Reference: kotlin/force-unwrap v0.29."
|
|
37099
|
+
});
|
|
37100
|
+
}
|
|
37101
|
+
return issues;
|
|
37102
|
+
}
|
|
37103
|
+
});
|
|
37104
|
+
|
|
37105
|
+
// src/rules/kotlin/hardcoded-credential.ts
|
|
37106
|
+
var KEY_REGEX = /\b(api[_-]?key|apikey|secret|token|password|auth|access[_-]?key|client[_-]?secret|private[_-]?key)\b\s*[=:]/i;
|
|
37107
|
+
var VALUE_REGEX = /["']([A-Za-z0-9_\-+/=.@*!]{16,})["']/;
|
|
37108
|
+
var FALSE_POSITIVES = /* @__PURE__ */ new Set([
|
|
37109
|
+
"passwordless",
|
|
37110
|
+
"tokenize",
|
|
37111
|
+
"tokenizer",
|
|
37112
|
+
"authorizationrequired",
|
|
37113
|
+
"authenticated",
|
|
37114
|
+
"authenticatortoken",
|
|
37115
|
+
"authtoken",
|
|
37116
|
+
"placeholder",
|
|
37117
|
+
// common in test fixtures
|
|
37118
|
+
"changeme"
|
|
37119
|
+
// placeholder, but still suspicious — kept for review
|
|
37120
|
+
]);
|
|
37121
|
+
var kotlinHardcodedCredentialRule = createRule({
|
|
37122
|
+
id: "kotlin/hardcoded-credential",
|
|
37123
|
+
category: "security",
|
|
37124
|
+
severity: "high",
|
|
37125
|
+
aiSpecific: false,
|
|
37126
|
+
description: "Hardcoded credential literal \u2014 use env vars or a secrets manager",
|
|
37127
|
+
create(_context) {
|
|
37128
|
+
return {};
|
|
37129
|
+
},
|
|
37130
|
+
analyze(_context, facts) {
|
|
37131
|
+
const issues = [];
|
|
37132
|
+
const source = facts.v2?._source;
|
|
37133
|
+
if (!source) return issues;
|
|
37134
|
+
if (!/\.kts?$/i.test(facts.filePath)) return issues;
|
|
37135
|
+
const lines = source.split("\n");
|
|
37136
|
+
for (let i = 0; i < lines.length; i++) {
|
|
37137
|
+
const line = lines[i];
|
|
37138
|
+
if (!KEY_REGEX.test(line)) continue;
|
|
37139
|
+
const valueMatch = VALUE_REGEX.exec(line);
|
|
37140
|
+
if (!valueMatch) continue;
|
|
37141
|
+
const value = valueMatch[1];
|
|
37142
|
+
if (FALSE_POSITIVES.has(value.toLowerCase())) continue;
|
|
37143
|
+
if (!/[a-zA-Z]/.test(value) || !/[0-9]/.test(value)) continue;
|
|
37144
|
+
if (value.startsWith("$")) continue;
|
|
37145
|
+
if (/\/test\//i.test(facts.filePath) || /\.test\.kts?$/i.test(facts.filePath)) continue;
|
|
37146
|
+
issues.push({
|
|
37147
|
+
ruleId: "kotlin/hardcoded-credential",
|
|
37148
|
+
category: "security",
|
|
37149
|
+
severity: "high",
|
|
37150
|
+
aiSpecific: false,
|
|
37151
|
+
message: `Hardcoded credential at line ${i + 1}`,
|
|
37152
|
+
line: i + 1,
|
|
37153
|
+
column: 1,
|
|
37154
|
+
advice: "Move the credential to an environment variable, a .env file that is .gitignore'd, or a secrets manager (Vault, AWS Secrets Manager, GCP Secret Manager). Hardcoded credentials are the #1 source of secret leaks on GitHub. Reference: kotlin/hardcoded-credential v0.29 (OWASP A07:2021 \u2014 Identification and Authentication Failures)."
|
|
37155
|
+
});
|
|
37156
|
+
}
|
|
37157
|
+
return issues;
|
|
37158
|
+
}
|
|
37159
|
+
});
|
|
37160
|
+
|
|
37066
37161
|
// src/rules/kotlin/object-singleton-misuse.ts
|
|
37067
37162
|
var OBJECT_DECL_REGEX = /\bobject\s+(?!\w*Companion\b)(\w+)\s*\{/g;
|
|
37068
37163
|
var kotlinObjectSingletonMisuseRule = createRule({
|
|
@@ -37113,8 +37208,46 @@ var kotlinObjectSingletonMisuseRule = createRule({
|
|
|
37113
37208
|
}
|
|
37114
37209
|
});
|
|
37115
37210
|
|
|
37211
|
+
// src/rules/kotlin/println-as-log.ts
|
|
37212
|
+
var PRINTLN_REGEX = /\bprintln\s*\(/g;
|
|
37213
|
+
var REAL_LOGGING_IMPORT_REGEX = /\bimport\s+(?:android\.util\.Log|org\.slf4j\.|io\.github\.oshai\.kotlinlogging|kotlin\.logging|co\.touchlab\.kermit|com\.github\.ajalt\.timber|org\.apache\.logging\.log4j)/;
|
|
37214
|
+
var kotlinPrintlnAsLogRule = createRule({
|
|
37215
|
+
id: "kotlin/println-as-log",
|
|
37216
|
+
category: "logic",
|
|
37217
|
+
severity: "low",
|
|
37218
|
+
aiSpecific: false,
|
|
37219
|
+
description: "println() used for logging \u2014 use slf4j, kermit, or android.util.Log",
|
|
37220
|
+
create(_context) {
|
|
37221
|
+
return {};
|
|
37222
|
+
},
|
|
37223
|
+
analyze(_context, facts) {
|
|
37224
|
+
const issues = [];
|
|
37225
|
+
const source = facts.v2?._source;
|
|
37226
|
+
if (!source) return issues;
|
|
37227
|
+
if (!/\.kts?$/i.test(facts.filePath)) return issues;
|
|
37228
|
+
if (/\/test\//i.test(facts.filePath) || /\.test\.kts?$/i.test(facts.filePath)) return issues;
|
|
37229
|
+
if (REAL_LOGGING_IMPORT_REGEX.test(source)) return issues;
|
|
37230
|
+
let m;
|
|
37231
|
+
PRINTLN_REGEX.lastIndex = 0;
|
|
37232
|
+
while ((m = PRINTLN_REGEX.exec(source)) !== null) {
|
|
37233
|
+
const line = source.slice(0, m.index).split("\n").length;
|
|
37234
|
+
issues.push({
|
|
37235
|
+
ruleId: "kotlin/println-as-log",
|
|
37236
|
+
category: "logic",
|
|
37237
|
+
severity: "low",
|
|
37238
|
+
aiSpecific: false,
|
|
37239
|
+
message: `println() as logger at line ${line}`,
|
|
37240
|
+
line,
|
|
37241
|
+
column: 1,
|
|
37242
|
+
advice: "Use a real logging library: slf4j (JVM), android.util.Log (Android), Timber (Android), kermit (multiplatform), or kotlin-logging. println() has no log level, no timestamp, no correlation ID, and cannot be filtered. Reference: kotlin/println-as-log v0.29."
|
|
37243
|
+
});
|
|
37244
|
+
}
|
|
37245
|
+
return issues;
|
|
37246
|
+
}
|
|
37247
|
+
});
|
|
37248
|
+
|
|
37116
37249
|
// src/rules/kotlin/println-debug.ts
|
|
37117
|
-
var
|
|
37250
|
+
var PRINTLN_REGEX2 = /^\s*println\s*\(/gm;
|
|
37118
37251
|
var DEFAULT_THRESHOLD = 1;
|
|
37119
37252
|
var kotlinPrintlnDebugRule = createRule({
|
|
37120
37253
|
id: "kotlin/println-debug",
|
|
@@ -37132,8 +37265,8 @@ var kotlinPrintlnDebugRule = createRule({
|
|
|
37132
37265
|
if (!/\.kts?$/i.test(facts.filePath)) return issues;
|
|
37133
37266
|
const matches = [];
|
|
37134
37267
|
let m;
|
|
37135
|
-
|
|
37136
|
-
while ((m =
|
|
37268
|
+
PRINTLN_REGEX2.lastIndex = 0;
|
|
37269
|
+
while ((m = PRINTLN_REGEX2.exec(source)) !== null) {
|
|
37137
37270
|
matches.push(m.index);
|
|
37138
37271
|
}
|
|
37139
37272
|
if (matches.length <= context.threshold) return issues;
|
|
@@ -37156,6 +37289,81 @@ var kotlinPrintlnDebugRule = createRule({
|
|
|
37156
37289
|
}
|
|
37157
37290
|
});
|
|
37158
37291
|
|
|
37292
|
+
// src/rules/kotlin/runblocking-misuse.ts
|
|
37293
|
+
var RUN_BLOCKING_REGEX = /\brunBlocking\s*[({]/g;
|
|
37294
|
+
var kotlinRunBlockingMisuseRule = createRule({
|
|
37295
|
+
id: "kotlin/runblocking-misuse",
|
|
37296
|
+
category: "perf",
|
|
37297
|
+
severity: "medium",
|
|
37298
|
+
aiSpecific: false,
|
|
37299
|
+
description: "runBlocking { ... } \u2014 blocks the calling thread; use coroutineScope {} or call suspend fun directly",
|
|
37300
|
+
create(_context) {
|
|
37301
|
+
return {};
|
|
37302
|
+
},
|
|
37303
|
+
analyze(_context, facts) {
|
|
37304
|
+
const issues = [];
|
|
37305
|
+
const source = facts.v2?._source;
|
|
37306
|
+
if (!source) return issues;
|
|
37307
|
+
if (!/\.kts?$/i.test(facts.filePath)) return issues;
|
|
37308
|
+
if (/\bfun\s+main\s*\(/.test(source)) return issues;
|
|
37309
|
+
let m;
|
|
37310
|
+
RUN_BLOCKING_REGEX.lastIndex = 0;
|
|
37311
|
+
while ((m = RUN_BLOCKING_REGEX.exec(source)) !== null) {
|
|
37312
|
+
const line = source.slice(0, m.index).split("\n").length;
|
|
37313
|
+
issues.push({
|
|
37314
|
+
ruleId: "kotlin/runblocking-misuse",
|
|
37315
|
+
category: "perf",
|
|
37316
|
+
severity: "medium",
|
|
37317
|
+
aiSpecific: false,
|
|
37318
|
+
message: `runBlocking { ... } at line ${line}`,
|
|
37319
|
+
line,
|
|
37320
|
+
column: 1,
|
|
37321
|
+
advice: 'runBlocking blocks the calling thread, defeating the purpose of coroutines. Use coroutineScope { } for structured concurrency, or call the suspend function directly. The Kotlin coroutines docs: runBlocking "should rarely (if ever) be used outside of main()". Reference: kotlin/runblocking-misuse v0.29.'
|
|
37322
|
+
});
|
|
37323
|
+
}
|
|
37324
|
+
return issues;
|
|
37325
|
+
}
|
|
37326
|
+
});
|
|
37327
|
+
|
|
37328
|
+
// src/rules/kotlin/sql-string-concat.ts
|
|
37329
|
+
var SQL_KEYWORD_REGEX = /\b(?:SELECT|INSERT\s+INTO|UPDATE|DELETE\s+FROM|CREATE\s+TABLE|DROP\s+TABLE|ALTER\s+TABLE)\b/i;
|
|
37330
|
+
var UNSAFE_REGEX = /(?:\+|\$\{)/;
|
|
37331
|
+
var SAFE_REGEX = /(?:PreparedStatement|setParameter|setString|setInt|setLong|bind|:name|:\\?\\?|\?\\s*,)/;
|
|
37332
|
+
var kotlinSqlStringConcatRule = createRule({
|
|
37333
|
+
id: "kotlin/sql-string-concat",
|
|
37334
|
+
category: "security",
|
|
37335
|
+
severity: "high",
|
|
37336
|
+
aiSpecific: false,
|
|
37337
|
+
description: "SQL query built via string concat / template \u2014 use PreparedStatement or setParameter()",
|
|
37338
|
+
create(_context) {
|
|
37339
|
+
return {};
|
|
37340
|
+
},
|
|
37341
|
+
analyze(_context, facts) {
|
|
37342
|
+
const issues = [];
|
|
37343
|
+
const source = facts.v2?._source;
|
|
37344
|
+
if (!source) return issues;
|
|
37345
|
+
if (!/\.kts?$/i.test(facts.filePath)) return issues;
|
|
37346
|
+
const lines = source.split("\n");
|
|
37347
|
+
for (let i = 0; i < lines.length; i++) {
|
|
37348
|
+
const line = lines[i];
|
|
37349
|
+
if (!SQL_KEYWORD_REGEX.test(line)) continue;
|
|
37350
|
+
if (!UNSAFE_REGEX.test(line)) continue;
|
|
37351
|
+
if (SAFE_REGEX.test(line)) continue;
|
|
37352
|
+
issues.push({
|
|
37353
|
+
ruleId: "kotlin/sql-string-concat",
|
|
37354
|
+
category: "security",
|
|
37355
|
+
severity: "high",
|
|
37356
|
+
aiSpecific: false,
|
|
37357
|
+
message: `SQL query built via string concat/template at line ${i + 1}`,
|
|
37358
|
+
line: i + 1,
|
|
37359
|
+
column: 1,
|
|
37360
|
+
advice: 'Use a PreparedStatement (JDBC), setParameter() (Exposed), or an ORM (Room, jOOQ). String concatenation or template interpolation into a SQL query is the canonical SQL-injection pattern \u2014 even "trusted" inputs (signed JWT, internal config) can be influenced by an attacker. Reference: kotlin/sql-string-concat v0.29 (OWASP A03:2021).'
|
|
37361
|
+
});
|
|
37362
|
+
}
|
|
37363
|
+
return issues;
|
|
37364
|
+
}
|
|
37365
|
+
});
|
|
37366
|
+
|
|
37159
37367
|
// src/rules/kotlin/string-concat-loop.ts
|
|
37160
37368
|
var STRING_CONCAT_REGEX = /\b(\w+)\s*=\s*\1\s*\+\s*[^;}]+[;}\n]/g;
|
|
37161
37369
|
var kotlinStringConcatLoopRule = createRule({
|
|
@@ -42718,8 +42926,13 @@ var builtinRules = [
|
|
|
42718
42926
|
goStructTagInconsistencyRule,
|
|
42719
42927
|
kotlinCoroutineGlobalScopeRule,
|
|
42720
42928
|
kotlinDataClassDefaultsOveruseRule,
|
|
42929
|
+
kotlinForceUnwrapRule,
|
|
42930
|
+
kotlinHardcodedCredentialRule,
|
|
42721
42931
|
kotlinObjectSingletonMisuseRule,
|
|
42932
|
+
kotlinPrintlnAsLogRule,
|
|
42722
42933
|
kotlinPrintlnDebugRule,
|
|
42934
|
+
kotlinRunBlockingMisuseRule,
|
|
42935
|
+
kotlinSqlStringConcatRule,
|
|
42723
42936
|
kotlinStringConcatLoopRule,
|
|
42724
42937
|
gapMonopolyRule,
|
|
42725
42938
|
mathElementUniformityRule,
|
|
@@ -44938,6 +45151,86 @@ var signal_strength_default = {
|
|
|
44938
45151
|
_v9Precision: 0.125,
|
|
44939
45152
|
defaultOff: true
|
|
44940
45153
|
},
|
|
45154
|
+
"kotlin/sql-string-concat": {
|
|
45155
|
+
recall: 469e-5,
|
|
45156
|
+
fpRate: 63e-4,
|
|
45157
|
+
ratio: 0.75,
|
|
45158
|
+
precision: 0.0556,
|
|
45159
|
+
lastCalibratedAt: "2026-07-03T00:00:00Z",
|
|
45160
|
+
verdict: "DORMANT",
|
|
45161
|
+
_calibrationNote: "v0.29: v9 Kotlin calibration (2698 neg, 213 pos). ratio=0.75 (DORMANT). 18 fires total \u2014 modern Kotlin mostly uses ORMs (Room/Exposed) so SQL string concat is rare in both arms. INSUFFICIENT_DATA: pos arm only 213 files. defaultOff: rule is loadable but invisible by default until v0.30 re-calibration with a larger pos arm.",
|
|
45162
|
+
aiSpecific: false,
|
|
45163
|
+
_v9Verdict: "DORMANT",
|
|
45164
|
+
_v9Lift: 0.75,
|
|
45165
|
+
_v9Recall: 469e-5,
|
|
45166
|
+
_v9FpRate: 63e-4,
|
|
45167
|
+
_v9Precision: 0.0556,
|
|
45168
|
+
defaultOff: true
|
|
45169
|
+
},
|
|
45170
|
+
"kotlin/hardcoded-credential": {
|
|
45171
|
+
recall: 0,
|
|
45172
|
+
fpRate: 0,
|
|
45173
|
+
ratio: 0,
|
|
45174
|
+
precision: 0,
|
|
45175
|
+
lastCalibratedAt: "2026-07-03T00:00:00Z",
|
|
45176
|
+
verdict: "DORMANT",
|
|
45177
|
+
_calibrationNote: "v0.29: v9 Kotlin calibration (2698 neg, 213 pos). 0 fires. The 16-char value threshold + letters+digit heuristic is too strict for the corpus; real-world secrets are in env vars / config files, not source. INSUFFICIENT_DATA: needs different corpus (CI configs, .env samples). defaultOff: loadable but invisible by default.",
|
|
45178
|
+
aiSpecific: false,
|
|
45179
|
+
_v9Verdict: "DORMANT",
|
|
45180
|
+
_v9Lift: 0,
|
|
45181
|
+
_v9Recall: 0,
|
|
45182
|
+
_v9FpRate: 0,
|
|
45183
|
+
_v9Precision: 0,
|
|
45184
|
+
defaultOff: true
|
|
45185
|
+
},
|
|
45186
|
+
"kotlin/runblocking-misuse": {
|
|
45187
|
+
recall: 0.10798,
|
|
45188
|
+
fpRate: 0.21534,
|
|
45189
|
+
ratio: 0.5,
|
|
45190
|
+
precision: 0.0381,
|
|
45191
|
+
lastCalibratedAt: "2026-07-03T00:00:00Z",
|
|
45192
|
+
verdict: "DORMANT",
|
|
45193
|
+
_calibrationNote: "v0.29: v9 Kotlin calibration (2698 neg, 213 pos). ratio=0.50 \u2014 fires 2x more on pre-2022 (neg) than post-2024 (pos). Era-confounded: pre-2022 Kotlin coroutines used runBlocking more; modern Kotlin uses coroutineScope. Same direction as kotlin/coroutine-global-scope (0.09). defaultOff: loadable but invisible by default until v0.30 re-calibration.",
|
|
45194
|
+
aiSpecific: false,
|
|
45195
|
+
_v9Verdict: "DORMANT",
|
|
45196
|
+
_v9Lift: 0.5,
|
|
45197
|
+
_v9Recall: 0.10798,
|
|
45198
|
+
_v9FpRate: 0.21534,
|
|
45199
|
+
_v9Precision: 0.0381,
|
|
45200
|
+
defaultOff: true
|
|
45201
|
+
},
|
|
45202
|
+
"kotlin/println-as-log": {
|
|
45203
|
+
recall: 0.08451,
|
|
45204
|
+
fpRate: 0.04596,
|
|
45205
|
+
ratio: 1.84,
|
|
45206
|
+
precision: 0.1268,
|
|
45207
|
+
lastCalibratedAt: "2026-07-03T00:00:00Z",
|
|
45208
|
+
verdict: "OK",
|
|
45209
|
+
_calibrationNote: "v0.29: v9 Kotlin calibration (2698 neg, 213 pos). ratio=1.84 (\u22651.5) \u2014 first rule in this entire session with a positive direction! Fires 1.84x more on post-2024 AI/demos than pre-2022 production. Precision=12.7% (below 50% USEFUL threshold); verdict=OK. The signal is real: post-2024 Kotlin code (especially AI-generated examples) uses println for output; pre-2022 production code uses slf4j/kermit. INSUFFICIENT_DATA: pos arm only 213 files. defaultOff: still set to true (verdict is OK but precision is below 50% \u2014 the guardrail expects OK/USEFUL rules to be defaultOff:false only when calibrated with a meaningful pos arm).",
|
|
45210
|
+
aiSpecific: false,
|
|
45211
|
+
_v9Verdict: "OK",
|
|
45212
|
+
_v9Lift: 1.84,
|
|
45213
|
+
_v9Recall: 0.08451,
|
|
45214
|
+
_v9FpRate: 0.04596,
|
|
45215
|
+
_v9Precision: 0.1268,
|
|
45216
|
+
defaultOff: true
|
|
45217
|
+
},
|
|
45218
|
+
"kotlin/force-unwrap": {
|
|
45219
|
+
recall: 0.11737,
|
|
45220
|
+
fpRate: 0.33284,
|
|
45221
|
+
ratio: 0.35,
|
|
45222
|
+
precision: 0.0271,
|
|
45223
|
+
lastCalibratedAt: "2026-07-03T00:00:00Z",
|
|
45224
|
+
verdict: "DORMANT",
|
|
45225
|
+
_calibrationNote: "v0.29: v9 Kotlin calibration (2698 neg, 213 pos). ratio=0.35 \u2014 fires 3x more on pre-2022 (neg) than post-2024 (pos). Era-confounded: pre-2022 Kotlin code uses !! freely; modern Kotlin relies on ?./?: and Result<T> wrappers. Stronger era signal than runblocking-misuse. defaultOff: loadable but invisible by default.",
|
|
45226
|
+
aiSpecific: false,
|
|
45227
|
+
_v9Verdict: "DORMANT",
|
|
45228
|
+
_v9Lift: 0.35,
|
|
45229
|
+
_v9Recall: 0.11737,
|
|
45230
|
+
_v9FpRate: 0.33284,
|
|
45231
|
+
_v9Precision: 0.0271,
|
|
45232
|
+
defaultOff: true
|
|
45233
|
+
},
|
|
44941
45234
|
"swift/force-unwrap": {
|
|
44942
45235
|
recall: 0,
|
|
44943
45236
|
fpRate: 0,
|