| 1 | package org.egothor.methodatlas.command; | |
| 2 | ||
| 3 | import java.io.IOException; | |
| 4 | import java.io.PrintWriter; | |
| 5 | import java.nio.charset.StandardCharsets; | |
| 6 | import java.nio.file.Files; | |
| 7 | import java.nio.file.Path; | |
| 8 | import java.util.ArrayList; | |
| 9 | import java.util.LinkedHashMap; | |
| 10 | import java.util.List; | |
| 11 | import java.util.Map; | |
| 12 | import java.util.Objects; | |
| 13 | import java.util.Optional; | |
| 14 | import java.util.logging.Level; | |
| 15 | import java.util.logging.Logger; | |
| 16 | import java.util.stream.Collectors; | |
| 17 | import java.util.stream.IntStream; | |
| 18 | ||
| 19 | import org.egothor.methodatlas.CliConfig; | |
| 20 | import org.egothor.methodatlas.ai.AiSuggestionEngine; | |
| 21 | import org.egothor.methodatlas.ai.AiSuggestionException; | |
| 22 | import org.egothor.methodatlas.ai.PromptBuilder; | |
| 23 | import org.egothor.methodatlas.ai.CredentialTriageVerdict; | |
| 24 | import org.egothor.methodatlas.api.DiscoveredMethod; | |
| 25 | import org.egothor.methodatlas.api.CredentialDetector; | |
| 26 | import org.egothor.methodatlas.api.CredentialDetectorConfig; | |
| 27 | import org.egothor.methodatlas.api.CredentialScanUnit; | |
| 28 | import org.egothor.methodatlas.api.TestDiscovery; | |
| 29 | import org.egothor.methodatlas.api.TestDiscoveryConfig; | |
| 30 | import org.egothor.methodatlas.emit.SarifEmitter; | |
| 31 | import org.egothor.methodatlas.emit.CredentialCsvEmitter; | |
| 32 | import org.egothor.methodatlas.emit.CredentialFinding; | |
| 33 | import org.egothor.methodatlas.emit.CredentialMasker; | |
| 34 | ||
| 35 | /** | |
| 36 | * Shared orchestration for the {@code -detect-secrets} feature. | |
| 37 | * | |
| 38 | * <p> | |
| 39 | * Two triage strategies are supported, both producing the same outputs (log, | |
| 40 | * secrets CSV, and — in SARIF mode — secret results embedded in the document): | |
| 41 | * </p> | |
| 42 | * <ul> | |
| 43 | * <li><b>Folded</b> (default scope with AI enabled): the command runs detection | |
| 44 | * up-front via {@link #detect(List)}, hands the resulting | |
| 45 | * {@link CredentialTriageContext} to the scan so each per-class classification | |
| 46 | * call <em>also</em> triages that class's candidates — the class source is | |
| 47 | * sent to the provider once. The command then calls | |
| 48 | * {@link #applyFoldedVerdicts(List, Map)} and {@link #emitFindings}.</li> | |
| 49 | * <li><b>Separate</b> ({@link #run(List, SarifEmitter)}): used with no AI, or | |
| 50 | * with a {@code -secrets-include} glob (which scans files outside the | |
| 51 | * discovered classes). Detection is followed by an optional dedicated triage | |
| 52 | * call per file.</li> | |
| 53 | * </ul> | |
| 54 | * | |
| 55 | * <p> | |
| 56 | * The deterministic detection itself never calls AI; a failed triage degrades to | |
| 57 | * unverified candidates. | |
| 58 | * </p> | |
| 59 | * | |
| 60 | * @since 4.1.0 | |
| 61 | */ | |
| 62 | final class CredentialDetectionRunner { | |
| 63 | ||
| 64 | private static final Logger LOG = Logger.getLogger(CredentialDetectionRunner.class.getName()); | |
| 65 | ||
| 66 | /** Default Shannon-entropy floor handed to the detectors. */ | |
| 67 | private static final double DEFAULT_ENTROPY = 4.0; | |
| 68 | ||
| 69 | /** Extension appended when deriving a SARIF artifact URI from an FQCN. */ | |
| 70 | private static final String JAVA_EXTENSION = ".java"; | |
| 71 | ||
| 72 | /** Grouping key used for findings that carry no fully qualified class name. */ | |
| 73 | private static final String NO_FQCN = ""; | |
| 74 | ||
| 75 | private final CliConfig cfg; | |
| 76 | private final TestDiscoveryConfig discoveryConfig; | |
| 77 | private final PluginLoader pluginLoader; | |
| 78 | private final ScanOrchestrator orchestrator; | |
| 79 | private final AiSuggestionEngine aiEngine; | |
| 80 | ||
| 81 | /** Whether triage is folded into the scan's classification call (set by {@link #prepare}). */ | |
| 82 | private boolean folded; | |
| 83 | /** Up-front detection result captured by {@link #prepare} for the folded path. */ | |
| 84 | private DetectionResult preparedDetection; | |
| 85 | /** Triage context handed to the scan in the folded path. */ | |
| 86 | private CredentialTriageContext preparedContext; | |
| 87 | ||
| 88 | /** | |
| 89 | * Creates a runner. | |
| 90 | * | |
| 91 | * @param cfg parsed CLI configuration; never {@code null} | |
| 92 | * @param discoveryConfig discovery configuration used to enumerate test classes | |
| 93 | * and build the attribution index; never {@code null} | |
| 94 | * @param pluginLoader loader used to resolve discovery providers and secret | |
| 95 | * detectors; never {@code null} | |
| 96 | * @param orchestrator orchestrator used to group discovered methods by file; | |
| 97 | * never {@code null} | |
| 98 | * @param aiEngine AI engine used for triage, or {@code null} when AI is | |
| 99 | * disabled (deterministic candidates are still emitted) | |
| 100 | */ | |
| 101 | /* default */ CredentialDetectionRunner(CliConfig cfg, TestDiscoveryConfig discoveryConfig, | |
| 102 | PluginLoader pluginLoader, ScanOrchestrator orchestrator, AiSuggestionEngine aiEngine) { | |
| 103 | this.cfg = cfg; | |
| 104 | this.discoveryConfig = discoveryConfig; | |
| 105 | this.pluginLoader = pluginLoader; | |
| 106 | this.orchestrator = orchestrator; | |
| 107 | this.aiEngine = aiEngine; | |
| 108 | } | |
| 109 | ||
| 110 | /** | |
| 111 | * Deterministic detection result, plus the per-class candidate spans needed to | |
| 112 | * fold triage into the scan and the per-file source used for separate triage. | |
| 113 | * | |
| 114 | * @param findings all deterministic findings (triage fields {@code null}) | |
| 115 | * @param candidatesByFqcn candidate spans per class, in finding order | |
| 116 | * @param sourceByFile file source text, for separate-call triage | |
| 117 | * @since 4.1.0 | |
| 118 | */ | |
| 119 | /* default */ record DetectionResult(List<CredentialFinding> findings, | |
| 120 | Map<String, List<PromptBuilder.CredentialCandidateRef>> candidatesByFqcn, | |
| 121 | Map<Path, String> sourceByFile) { | |
| 122 | } | |
| 123 | ||
| 124 | // --------------------------------------------------------------------- | |
| 125 | // Command-facing lifecycle (folded or separate, decided here) | |
| 126 | // --------------------------------------------------------------------- | |
| 127 | ||
| 128 | /** | |
| 129 | * Prepares credential detection before the scan. When triage can be folded | |
| 130 | * into the per-class classification call (AI enabled, default test-class scope, | |
| 131 | * and {@code -secrets-separate-llm} not set), this runs deterministic detection | |
| 132 | * up-front and returns the {@link CredentialTriageContext} the scan must thread | |
| 133 | * through so the class source is sent to the provider once. Otherwise returns | |
| 134 | * {@code null} and {@link #finish} performs detection (and any separate triage) | |
| 135 | * after the scan. | |
| 136 | * | |
| 137 | * @param roots scan roots; never {@code null} | |
| 138 | * @return the triage context to pass to the scan, or {@code null} when not folding | |
| 139 | * @throws IOException if up-front detection fails | |
| 140 | */ | |
| 141 | /* default */ CredentialTriageContext prepare(List<Path> roots) throws IOException { | |
| 142 |
6
1. prepare : removed conditional - replaced equality check with true → NO_COVERAGE 2. prepare : removed conditional - replaced equality check with true → NO_COVERAGE 3. prepare : removed conditional - replaced equality check with true → NO_COVERAGE 4. prepare : removed conditional - replaced equality check with false → NO_COVERAGE 5. prepare : removed conditional - replaced equality check with false → NO_COVERAGE 6. prepare : removed conditional - replaced equality check with false → NO_COVERAGE |
this.folded = aiEngine != null && cfg.secretsInclude() == null && !cfg.secretsSeparateLlm(); |
| 143 |
2
1. prepare : removed conditional - replaced equality check with true → NO_COVERAGE 2. prepare : removed conditional - replaced equality check with false → NO_COVERAGE |
if (folded) { |
| 144 | this.preparedDetection = detect(roots); | |
| 145 | this.preparedContext = toContext(preparedDetection); | |
| 146 |
1
1. prepare : replaced return value with null for org/egothor/methodatlas/command/CredentialDetectionRunner::prepare → NO_COVERAGE |
return preparedContext; |
| 147 | } | |
| 148 | return null; | |
| 149 | } | |
| 150 | ||
| 151 | /** | |
| 152 | * Completes credential detection after the scan: in the folded path it merges | |
| 153 | * the verdicts the scan collected and emits; otherwise it runs detection plus | |
| 154 | * optional separate-call triage and emits. | |
| 155 | * | |
| 156 | * @param roots scan roots; never {@code null} | |
| 157 | * @param sarifEmitter SARIF emitter to record findings into, or {@code null} | |
| 158 | * @throws IOException if collecting files or writing the CSV fails | |
| 159 | */ | |
| 160 | /* default */ void finish(List<Path> roots, SarifEmitter sarifEmitter) throws IOException { | |
| 161 |
2
1. finish : removed conditional - replaced equality check with true → NO_COVERAGE 2. finish : removed conditional - replaced equality check with false → NO_COVERAGE |
if (folded) { |
| 162 |
1
1. finish : removed call to org/egothor/methodatlas/command/CredentialDetectionRunner::emitFindings → NO_COVERAGE |
emitFindings(applyFoldedVerdicts(preparedDetection.findings(), |
| 163 | preparedContext.verdictsByFqcn()), sarifEmitter); | |
| 164 | } else { | |
| 165 |
1
1. finish : removed call to org/egothor/methodatlas/command/CredentialDetectionRunner::run → NO_COVERAGE |
run(roots, sarifEmitter); |
| 166 | } | |
| 167 | } | |
| 168 | ||
| 169 | // --------------------------------------------------------------------- | |
| 170 | // Separate-call path | |
| 171 | // --------------------------------------------------------------------- | |
| 172 | ||
| 173 | /** | |
| 174 | * Runs detection, optional separate-call triage, and emission. Used when AI is | |
| 175 | * disabled or a {@code -secrets-include} glob is active. | |
| 176 | * | |
| 177 | * @param roots scan roots; never {@code null} | |
| 178 | * @param sarifEmitter SARIF emitter to record findings into, or {@code null} | |
| 179 | * @throws IOException if collecting files or writing the CSV fails | |
| 180 | */ | |
| 181 | /* default */ void run(List<Path> roots, SarifEmitter sarifEmitter) throws IOException { | |
| 182 | DetectionResult dr = detect(roots); | |
| 183 |
2
1. run : removed conditional - replaced equality check with false → NO_COVERAGE 2. run : removed conditional - replaced equality check with true → NO_COVERAGE |
List<CredentialFinding> triaged = aiEngine == null |
| 184 | ? dr.findings() | |
| 185 | : triageSeparately(dr.findings(), dr.sourceByFile()); | |
| 186 |
1
1. run : removed call to org/egothor/methodatlas/command/CredentialDetectionRunner::emitFindings → NO_COVERAGE |
emitFindings(triaged, sarifEmitter); |
| 187 | } | |
| 188 | ||
| 189 | // --------------------------------------------------------------------- | |
| 190 | // Folded path (used by the command together with the scan) | |
| 191 | // --------------------------------------------------------------------- | |
| 192 | ||
| 193 | /** | |
| 194 | * Runs deterministic detection only, returning the findings plus the data the | |
| 195 | * folded path needs to triage during the scan. | |
| 196 | * | |
| 197 | * @param roots scan roots; never {@code null} | |
| 198 | * @return the detection result; never {@code null} | |
| 199 | * @throws IOException if collecting files fails | |
| 200 | */ | |
| 201 | /* default */ DetectionResult detect(List<Path> roots) throws IOException { | |
| 202 | Map<Path, List<DiscoveredMethod>> byFile = discoverByFile(roots); | |
| 203 | Map<Path, List<DetectCredentialsStage.MethodRange>> attribution = toAttribution(byFile); | |
| 204 | List<CredentialScanUnit> units = selectUnits(roots, byFile); | |
| 205 | Map<Path, String> sourceByFile = units.stream() | |
| 206 |
1
1. lambda$detect$0 : replaced return value with "" for org/egothor/methodatlas/command/CredentialDetectionRunner::lambda$detect$0 → NO_COVERAGE |
.collect(Collectors.toMap(CredentialScanUnit::filePath, CredentialScanUnit::source, (a, b) -> a)); |
| 207 | List<CredentialFinding> findings = runDetectors(units, attribution); | |
| 208 |
1
1. detect : replaced return value with null for org/egothor/methodatlas/command/CredentialDetectionRunner::detect → NO_COVERAGE |
return new DetectionResult(findings, candidatesByFqcn(findings), sourceByFile); |
| 209 | } | |
| 210 | ||
| 211 | /** | |
| 212 | * Wraps a detection result in a triage context for the scan to fill. | |
| 213 | * | |
| 214 | * @param detection the detection result; never {@code null} | |
| 215 | * @return a context carrying the per-class candidates | |
| 216 | */ | |
| 217 | /* default */ CredentialTriageContext toContext(DetectionResult detection) { | |
| 218 |
1
1. toContext : replaced return value with null for org/egothor/methodatlas/command/CredentialDetectionRunner::toContext → NO_COVERAGE |
return new CredentialTriageContext(detection.candidatesByFqcn()); |
| 219 | } | |
| 220 | ||
| 221 | /** | |
| 222 | * Merges the verdicts the scan collected (keyed by class) back into the | |
| 223 | * findings, by class and candidate index. | |
| 224 | * | |
| 225 | * @param findings the deterministic findings; never {@code null} | |
| 226 | * @param verdictsByFqcn verdicts collected during the folded scan; never {@code null} | |
| 227 | * @return findings with triage fields populated where a verdict exists | |
| 228 | */ | |
| 229 | /* default */ List<CredentialFinding> applyFoldedVerdicts(List<CredentialFinding> findings, | |
| 230 | Map<String, List<CredentialTriageVerdict>> verdictsByFqcn) { | |
| 231 | List<CredentialFinding> out = new ArrayList<>(findings.size()); | |
| 232 |
1
1. applyFoldedVerdicts : removed call to java/util/Map::forEach → NO_COVERAGE |
groupByFqcn(findings).forEach((fqcn, group) -> { |
| 233 | Map<Integer, CredentialTriageVerdict> byIndex = verdictsByFqcn.getOrDefault(fqcn, List.of()).stream() | |
| 234 |
2
1. lambda$applyFoldedVerdicts$2 : replaced return value with null for org/egothor/methodatlas/command/CredentialDetectionRunner::lambda$applyFoldedVerdicts$2 → NO_COVERAGE 2. lambda$applyFoldedVerdicts$1 : replaced return value with null for org/egothor/methodatlas/command/CredentialDetectionRunner::lambda$applyFoldedVerdicts$1 → NO_COVERAGE |
.collect(Collectors.toMap(CredentialTriageVerdict::candidateIndex, v -> v, (a, b) -> a)); |
| 235 | out.addAll(DetectCredentialsStage.mergeVerdicts(group, byIndex)); | |
| 236 | }); | |
| 237 |
1
1. applyFoldedVerdicts : replaced return value with Collections.emptyList for org/egothor/methodatlas/command/CredentialDetectionRunner::applyFoldedVerdicts → NO_COVERAGE |
return out; |
| 238 | } | |
| 239 | ||
| 240 | /** | |
| 241 | * Applies the min-score filter and emits all outputs. | |
| 242 | * | |
| 243 | * @param findings findings to emit; never {@code null} | |
| 244 | * @param sarifEmitter SARIF emitter to record findings into, or {@code null} | |
| 245 | * @throws IOException if writing the CSV fails | |
| 246 | */ | |
| 247 | /* default */ void emitFindings(List<CredentialFinding> findings, SarifEmitter sarifEmitter) throws IOException { | |
| 248 | List<CredentialFinding> kept = filterByMinScore(findings); | |
| 249 |
1
1. emitFindings : removed call to org/egothor/methodatlas/command/CredentialDetectionRunner::logSummary → NO_COVERAGE |
logSummary(kept); |
| 250 |
1
1. emitFindings : removed call to org/egothor/methodatlas/command/CredentialDetectionRunner::logFindings → NO_COVERAGE |
logFindings(kept); |
| 251 |
1
1. emitFindings : removed call to org/egothor/methodatlas/command/CredentialDetectionRunner::recordIntoSarif → NO_COVERAGE |
recordIntoSarif(kept, sarifEmitter); |
| 252 |
1
1. emitFindings : removed call to org/egothor/methodatlas/command/CredentialDetectionRunner::writeCsv → NO_COVERAGE |
writeCsv(kept); |
| 253 | } | |
| 254 | ||
| 255 | // --------------------------------------------------------------------- | |
| 256 | // Internals | |
| 257 | // --------------------------------------------------------------------- | |
| 258 | ||
| 259 | private Map<Path, List<DiscoveredMethod>> discoverByFile(List<Path> roots) throws IOException { | |
| 260 | List<TestDiscovery> providers = pluginLoader.loadProviders(discoveryConfig); | |
| 261 | try { | |
| 262 |
1
1. discoverByFile : replaced return value with Collections.emptyMap for org/egothor/methodatlas/command/CredentialDetectionRunner::discoverByFile → NO_COVERAGE |
return orchestrator.collectMethodsByFile(roots, providers); |
| 263 | } finally { | |
| 264 |
1
1. discoverByFile : removed call to org/egothor/methodatlas/command/PluginLoader::closeAll → NO_COVERAGE |
pluginLoader.closeAll(providers); |
| 265 | } | |
| 266 | } | |
| 267 | ||
| 268 | private static Map<Path, List<DetectCredentialsStage.MethodRange>> toAttribution( | |
| 269 | Map<Path, List<DiscoveredMethod>> byFile) { | |
| 270 | Map<Path, List<DetectCredentialsStage.MethodRange>> attribution = new LinkedHashMap<>(); | |
| 271 |
1
1. toAttribution : removed call to java/util/Map::forEach → NO_COVERAGE |
byFile.forEach((file, methods) -> attribution.put(file.toAbsolutePath(), methods.stream() |
| 272 |
1
1. lambda$toAttribution$4 : replaced return value with null for org/egothor/methodatlas/command/CredentialDetectionRunner::lambda$toAttribution$4 → NO_COVERAGE |
.map(m -> new DetectCredentialsStage.MethodRange(m.method(), m.beginLine(), m.endLine())) |
| 273 | .toList())); | |
| 274 |
1
1. toAttribution : replaced return value with Collections.emptyMap for org/egothor/methodatlas/command/CredentialDetectionRunner::toAttribution → NO_COVERAGE |
return attribution; |
| 275 | } | |
| 276 | ||
| 277 | private List<CredentialScanUnit> selectUnits(List<Path> roots, | |
| 278 | Map<Path, List<DiscoveredMethod>> byFile) { | |
| 279 |
2
1. selectUnits : removed conditional - replaced equality check with false → NO_COVERAGE 2. selectUnits : removed conditional - replaced equality check with true → NO_COVERAGE |
if (cfg.secretsInclude() != null) { |
| 280 |
1
1. selectUnits : replaced return value with Collections.emptyList for org/egothor/methodatlas/command/CredentialDetectionRunner::selectUnits → NO_COVERAGE |
return new CredentialScanUnitSource(cfg.fileSuffixes(), cfg.secretsInclude()).collect(roots); |
| 281 | } | |
| 282 |
1
1. selectUnits : replaced return value with Collections.emptyList for org/egothor/methodatlas/command/CredentialDetectionRunner::selectUnits → NO_COVERAGE |
return byFile.entrySet().stream() |
| 283 |
1
1. lambda$selectUnits$6 : replaced return value with null for org/egothor/methodatlas/command/CredentialDetectionRunner::lambda$selectUnits$6 → NO_COVERAGE |
.map(entry -> toUnit(entry.getKey(), entry.getValue())) |
| 284 | .filter(Objects::nonNull) | |
| 285 | .toList(); | |
| 286 | } | |
| 287 | ||
| 288 | private static CredentialScanUnit toUnit(Path file, List<DiscoveredMethod> methods) { | |
| 289 |
2
1. toUnit : removed conditional - replaced equality check with false → NO_COVERAGE 2. toUnit : removed conditional - replaced equality check with true → NO_COVERAGE |
if (methods.isEmpty()) { |
| 290 | return null; | |
| 291 | } | |
| 292 | Path abs = file.toAbsolutePath(); | |
| 293 | try { | |
| 294 | String text = Files.readString(abs, StandardCharsets.UTF_8); | |
| 295 |
1
1. toUnit : replaced return value with null for org/egothor/methodatlas/command/CredentialDetectionRunner::toUnit → NO_COVERAGE |
return new CredentialScanUnit(abs, methods.get(0).fqcn(), text, |
| 296 | CredentialScanUnitSource.languageOf(abs)); | |
| 297 | } catch (IOException e) { | |
| 298 | LOG.log(Level.FINE, e, () -> "Skipping unreadable discovered file: " + abs); | |
| 299 | return null; | |
| 300 | } | |
| 301 | } | |
| 302 | ||
| 303 | private List<CredentialFinding> runDetectors(List<CredentialScanUnit> units, | |
| 304 | Map<Path, List<DetectCredentialsStage.MethodRange>> attribution) { | |
| 305 | CredentialDetectorConfig sdc = new CredentialDetectorConfig( | |
| 306 | DEFAULT_ENTROPY, Optional.ofNullable(cfg.secretsRules()), Map.of()); | |
| 307 | List<CredentialDetector> detectors = pluginLoader.loadCredentialDetectors(sdc); | |
| 308 | try { | |
| 309 |
1
1. runDetectors : replaced return value with Collections.emptyList for org/egothor/methodatlas/command/CredentialDetectionRunner::runDetectors → NO_COVERAGE |
return new DetectCredentialsStage(detectors, attribution).run(units); |
| 310 | } finally { | |
| 311 |
1
1. runDetectors : removed call to org/egothor/methodatlas/command/PluginLoader::closeAllCredentialDetectors → NO_COVERAGE |
pluginLoader.closeAllCredentialDetectors(detectors); |
| 312 | } | |
| 313 | } | |
| 314 | ||
| 315 | private static Map<String, List<CredentialFinding>> groupByFqcn(List<CredentialFinding> findings) { | |
| 316 |
1
1. groupByFqcn : replaced return value with Collections.emptyMap for org/egothor/methodatlas/command/CredentialDetectionRunner::groupByFqcn → NO_COVERAGE |
return findings.stream().collect(Collectors.groupingBy( |
| 317 |
3
1. lambda$groupByFqcn$8 : removed conditional - replaced equality check with false → NO_COVERAGE 2. lambda$groupByFqcn$8 : removed conditional - replaced equality check with true → NO_COVERAGE 3. lambda$groupByFqcn$8 : replaced return value with "" for org/egothor/methodatlas/command/CredentialDetectionRunner::lambda$groupByFqcn$8 → NO_COVERAGE |
f -> f.fqcn() == null ? NO_FQCN : f.fqcn(), LinkedHashMap::new, Collectors.toList())); |
| 318 | } | |
| 319 | ||
| 320 | private static Map<String, List<PromptBuilder.CredentialCandidateRef>> candidatesByFqcn( | |
| 321 | List<CredentialFinding> findings) { | |
| 322 | Map<String, List<PromptBuilder.CredentialCandidateRef>> byFqcn = new LinkedHashMap<>(); | |
| 323 |
1
1. candidatesByFqcn : removed call to java/util/Map::forEach → NO_COVERAGE |
groupByFqcn(findings).forEach((fqcn, group) -> { |
| 324 |
2
1. lambda$candidatesByFqcn$10 : removed conditional - replaced equality check with true → NO_COVERAGE 2. lambda$candidatesByFqcn$10 : removed conditional - replaced equality check with false → NO_COVERAGE |
if (!NO_FQCN.equals(fqcn)) { |
| 325 | byFqcn.put(fqcn, IntStream.range(0, group.size()) | |
| 326 |
1
1. lambda$candidatesByFqcn$9 : replaced return value with null for org/egothor/methodatlas/command/CredentialDetectionRunner::lambda$candidatesByFqcn$9 → NO_COVERAGE |
.mapToObj(i -> new PromptBuilder.CredentialCandidateRef(i, |
| 327 | group.get(i).candidate().beginLine(), group.get(i).candidate().matchedValue())) | |
| 328 | .toList()); | |
| 329 | } | |
| 330 | }); | |
| 331 |
1
1. candidatesByFqcn : replaced return value with Collections.emptyMap for org/egothor/methodatlas/command/CredentialDetectionRunner::candidatesByFqcn → NO_COVERAGE |
return byFqcn; |
| 332 | } | |
| 333 | ||
| 334 | private List<CredentialFinding> triageSeparately(List<CredentialFinding> findings, Map<Path, String> sourceByFile) { | |
| 335 |
2
1. triageSeparately : removed conditional - replaced equality check with true → NO_COVERAGE 2. triageSeparately : removed conditional - replaced equality check with false → NO_COVERAGE |
if (findings.isEmpty()) { |
| 336 |
1
1. triageSeparately : replaced return value with Collections.emptyList for org/egothor/methodatlas/command/CredentialDetectionRunner::triageSeparately → NO_COVERAGE |
return findings; |
| 337 | } | |
| 338 | Map<Path, List<CredentialFinding>> byFile = findings.stream() | |
| 339 | .collect(Collectors.groupingBy(CredentialFinding::filePath, LinkedHashMap::new, Collectors.toList())); | |
| 340 | List<CredentialFinding> result = new ArrayList<>(findings.size()); | |
| 341 |
1
1. triageSeparately : removed call to java/util/Map::forEach → NO_COVERAGE |
byFile.forEach((file, group) -> |
| 342 | result.addAll(triageGroup(file, group, sourceByFile.getOrDefault(file, "")))); | |
| 343 |
1
1. triageSeparately : replaced return value with Collections.emptyList for org/egothor/methodatlas/command/CredentialDetectionRunner::triageSeparately → NO_COVERAGE |
return result; |
| 344 | } | |
| 345 | ||
| 346 | private List<CredentialFinding> triageGroup(Path file, List<CredentialFinding> group, String source) { | |
| 347 |
2
1. triageGroup : removed conditional - replaced equality check with false → NO_COVERAGE 2. triageGroup : removed conditional - replaced equality check with true → NO_COVERAGE |
String fqcn = group.get(0).fqcn() != null |
| 348 | ? group.get(0).fqcn() | |
| 349 | : file.toString().replace('\\', '/'); | |
| 350 | List<PromptBuilder.CredentialCandidateRef> refs = IntStream.range(0, group.size()) | |
| 351 |
1
1. lambda$triageGroup$12 : replaced return value with null for org/egothor/methodatlas/command/CredentialDetectionRunner::lambda$triageGroup$12 → NO_COVERAGE |
.mapToObj(i -> new PromptBuilder.CredentialCandidateRef(i, |
| 352 | group.get(i).candidate().beginLine(), group.get(i).candidate().matchedValue())) | |
| 353 | .toList(); | |
| 354 | try { | |
| 355 | List<CredentialTriageVerdict> verdicts = aiEngine.triageSecrets(fqcn, source, refs); | |
| 356 | Map<Integer, CredentialTriageVerdict> byIndex = verdicts.stream() | |
| 357 |
2
1. lambda$triageGroup$14 : replaced return value with null for org/egothor/methodatlas/command/CredentialDetectionRunner::lambda$triageGroup$14 → NO_COVERAGE 2. lambda$triageGroup$13 : replaced return value with null for org/egothor/methodatlas/command/CredentialDetectionRunner::lambda$triageGroup$13 → NO_COVERAGE |
.collect(Collectors.toMap(CredentialTriageVerdict::candidateIndex, v -> v, (a, b) -> a)); |
| 358 |
1
1. triageGroup : replaced return value with Collections.emptyList for org/egothor/methodatlas/command/CredentialDetectionRunner::triageGroup → NO_COVERAGE |
return DetectCredentialsStage.mergeVerdicts(group, byIndex); |
| 359 | } catch (AiSuggestionException e) { | |
| 360 | LOG.log(Level.WARNING, e, | |
| 361 |
1
1. lambda$triageGroup$15 : replaced return value with "" for org/egothor/methodatlas/command/CredentialDetectionRunner::lambda$triageGroup$15 → NO_COVERAGE |
() -> "Secret triage failed for " + fqcn + "; emitting unverified candidates"); |
| 362 |
1
1. triageGroup : replaced return value with Collections.emptyList for org/egothor/methodatlas/command/CredentialDetectionRunner::triageGroup → NO_COVERAGE |
return group; |
| 363 | } | |
| 364 | } | |
| 365 | ||
| 366 | private List<CredentialFinding> filterByMinScore(List<CredentialFinding> findings) { | |
| 367 | double minScore = cfg.secretsMinScore(); | |
| 368 | List<CredentialFinding> kept = new ArrayList<>(findings.size()); | |
| 369 | for (CredentialFinding f : findings) { | |
| 370 |
5
1. filterByMinScore : removed conditional - replaced comparison check with true → NO_COVERAGE 2. filterByMinScore : removed conditional - replaced equality check with false → NO_COVERAGE 3. filterByMinScore : removed conditional - replaced comparison check with false → NO_COVERAGE 4. filterByMinScore : changed conditional boundary → NO_COVERAGE 5. filterByMinScore : removed conditional - replaced equality check with true → NO_COVERAGE |
if (f.credibilityScore() == null || f.credibilityScore() >= minScore) { |
| 371 | kept.add(f); | |
| 372 | } | |
| 373 | } | |
| 374 |
1
1. filterByMinScore : replaced return value with Collections.emptyList for org/egothor/methodatlas/command/CredentialDetectionRunner::filterByMinScore → NO_COVERAGE |
return kept; |
| 375 | } | |
| 376 | ||
| 377 | private static void logSummary(List<CredentialFinding> kept) { | |
| 378 | if (LOG.isLoggable(Level.INFO)) { | |
| 379 | long files = kept.stream().map(CredentialFinding::filePath).distinct().count(); | |
| 380 | LOG.log(Level.INFO, "Credential detection: {0} finding(s) across {1} file(s)", | |
| 381 | new Object[] { kept.size(), files }); | |
| 382 | } | |
| 383 | } | |
| 384 | ||
| 385 | private void logFindings(List<CredentialFinding> kept) { | |
| 386 | for (CredentialFinding f : kept) { | |
| 387 | String raw = f.candidate().matchedValue(); | |
| 388 |
2
1. logFindings : removed conditional - replaced equality check with false → NO_COVERAGE 2. logFindings : removed conditional - replaced equality check with true → NO_COVERAGE |
String snippet = cfg.secretsShowValues() ? raw : CredentialMasker.mask(raw); |
| 389 | // Supplier form is lazy: the message is only assembled when INFO is loggable. | |
| 390 | LOG.info(() -> " " + f.filePath().toString().replace('\\', '/') | |
| 391 | + ":" + f.candidate().beginLine() | |
| 392 | + " [" + f.candidate().ruleId() + "] " + snippet); | |
| 393 | } | |
| 394 | } | |
| 395 | ||
| 396 | private void recordIntoSarif(List<CredentialFinding> kept, SarifEmitter sarifEmitter) { | |
| 397 |
2
1. recordIntoSarif : removed conditional - replaced equality check with true → NO_COVERAGE 2. recordIntoSarif : removed conditional - replaced equality check with false → NO_COVERAGE |
if (sarifEmitter == null) { |
| 398 | return; | |
| 399 | } | |
| 400 | for (CredentialFinding f : kept) { | |
| 401 |
1
1. recordIntoSarif : removed call to org/egothor/methodatlas/emit/SarifEmitter::recordSecret → NO_COVERAGE |
sarifEmitter.recordSecret(fileUri(f), f); |
| 402 | } | |
| 403 | } | |
| 404 | ||
| 405 | private static String fileUri(CredentialFinding f) { | |
| 406 |
2
1. fileUri : removed conditional - replaced equality check with true → NO_COVERAGE 2. fileUri : removed conditional - replaced equality check with false → NO_COVERAGE |
if (f.fqcn() != null) { |
| 407 |
1
1. fileUri : replaced return value with "" for org/egothor/methodatlas/command/CredentialDetectionRunner::fileUri → NO_COVERAGE |
return f.fqcn().replace('.', '/') + JAVA_EXTENSION; |
| 408 | } | |
| 409 |
1
1. fileUri : replaced return value with "" for org/egothor/methodatlas/command/CredentialDetectionRunner::fileUri → NO_COVERAGE |
return f.filePath().toString().replace('\\', '/'); |
| 410 | } | |
| 411 | ||
| 412 | private void writeCsv(List<CredentialFinding> kept) throws IOException { | |
| 413 | try (PrintWriter w = new PrintWriter(Files.newBufferedWriter(cfg.secretsOut()))) { | |
| 414 |
1
1. writeCsv : removed call to org/egothor/methodatlas/emit/CredentialCsvEmitter::flush → NO_COVERAGE |
new CredentialCsvEmitter(cfg.secretsShowValues()).flush(w, kept); |
| 415 | } | |
| 416 | } | |
| 417 | } | |
Mutations | ||
| 142 |
1.1 2.2 3.3 4.4 5.5 6.6 |
|
| 143 |
1.1 2.2 |
|
| 146 |
1.1 |
|
| 161 |
1.1 2.2 |
|
| 162 |
1.1 |
|
| 165 |
1.1 |
|
| 183 |
1.1 2.2 |
|
| 186 |
1.1 |
|
| 206 |
1.1 |
|
| 208 |
1.1 |
|
| 218 |
1.1 |
|
| 232 |
1.1 |
|
| 234 |
1.1 2.2 |
|
| 237 |
1.1 |
|
| 249 |
1.1 |
|
| 250 |
1.1 |
|
| 251 |
1.1 |
|
| 252 |
1.1 |
|
| 262 |
1.1 |
|
| 264 |
1.1 |
|
| 271 |
1.1 |
|
| 272 |
1.1 |
|
| 274 |
1.1 |
|
| 279 |
1.1 2.2 |
|
| 280 |
1.1 |
|
| 282 |
1.1 |
|
| 283 |
1.1 |
|
| 289 |
1.1 2.2 |
|
| 295 |
1.1 |
|
| 309 |
1.1 |
|
| 311 |
1.1 |
|
| 316 |
1.1 |
|
| 317 |
1.1 2.2 3.3 |
|
| 323 |
1.1 |
|
| 324 |
1.1 2.2 |
|
| 326 |
1.1 |
|
| 331 |
1.1 |
|
| 335 |
1.1 2.2 |
|
| 336 |
1.1 |
|
| 341 |
1.1 |
|
| 343 |
1.1 |
|
| 347 |
1.1 2.2 |
|
| 351 |
1.1 |
|
| 357 |
1.1 2.2 |
|
| 358 |
1.1 |
|
| 361 |
1.1 |
|
| 362 |
1.1 |
|
| 370 |
1.1 2.2 3.3 4.4 5.5 |
|
| 374 |
1.1 |
|
| 388 |
1.1 2.2 |
|
| 397 |
1.1 2.2 |
|
| 401 |
1.1 |
|
| 406 |
1.1 2.2 |
|
| 407 |
1.1 |
|
| 409 |
1.1 |
|
| 414 |
1.1 |