CredentialDetectionRunner.java

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
Location : prepare
Killed by : none
removed conditional - replaced equality check with true → NO_COVERAGE

2.2
Location : prepare
Killed by : none
removed conditional - replaced equality check with true → NO_COVERAGE

3.3
Location : prepare
Killed by : none
removed conditional - replaced equality check with true → NO_COVERAGE

4.4
Location : prepare
Killed by : none
removed conditional - replaced equality check with false → NO_COVERAGE

5.5
Location : prepare
Killed by : none
removed conditional - replaced equality check with false → NO_COVERAGE

6.6
Location : prepare
Killed by : none
removed conditional - replaced equality check with false → NO_COVERAGE

143

1.1
Location : prepare
Killed by : none
removed conditional - replaced equality check with true → NO_COVERAGE

2.2
Location : prepare
Killed by : none
removed conditional - replaced equality check with false → NO_COVERAGE

146

1.1
Location : prepare
Killed by : none
replaced return value with null for org/egothor/methodatlas/command/CredentialDetectionRunner::prepare → NO_COVERAGE

161

1.1
Location : finish
Killed by : none
removed conditional - replaced equality check with true → NO_COVERAGE

2.2
Location : finish
Killed by : none
removed conditional - replaced equality check with false → NO_COVERAGE

162

1.1
Location : finish
Killed by : none
removed call to org/egothor/methodatlas/command/CredentialDetectionRunner::emitFindings → NO_COVERAGE

165

1.1
Location : finish
Killed by : none
removed call to org/egothor/methodatlas/command/CredentialDetectionRunner::run → NO_COVERAGE

183

1.1
Location : run
Killed by : none
removed conditional - replaced equality check with false → NO_COVERAGE

2.2
Location : run
Killed by : none
removed conditional - replaced equality check with true → NO_COVERAGE

186

1.1
Location : run
Killed by : none
removed call to org/egothor/methodatlas/command/CredentialDetectionRunner::emitFindings → NO_COVERAGE

206

1.1
Location : lambda$detect$0
Killed by : none
replaced return value with "" for org/egothor/methodatlas/command/CredentialDetectionRunner::lambda$detect$0 → NO_COVERAGE

208

1.1
Location : detect
Killed by : none
replaced return value with null for org/egothor/methodatlas/command/CredentialDetectionRunner::detect → NO_COVERAGE

218

1.1
Location : toContext
Killed by : none
replaced return value with null for org/egothor/methodatlas/command/CredentialDetectionRunner::toContext → NO_COVERAGE

232

1.1
Location : applyFoldedVerdicts
Killed by : none
removed call to java/util/Map::forEach → NO_COVERAGE

234

1.1
Location : lambda$applyFoldedVerdicts$2
Killed by : none
replaced return value with null for org/egothor/methodatlas/command/CredentialDetectionRunner::lambda$applyFoldedVerdicts$2 → NO_COVERAGE

2.2
Location : lambda$applyFoldedVerdicts$1
Killed by : none
replaced return value with null for org/egothor/methodatlas/command/CredentialDetectionRunner::lambda$applyFoldedVerdicts$1 → NO_COVERAGE

237

1.1
Location : applyFoldedVerdicts
Killed by : none
replaced return value with Collections.emptyList for org/egothor/methodatlas/command/CredentialDetectionRunner::applyFoldedVerdicts → NO_COVERAGE

249

1.1
Location : emitFindings
Killed by : none
removed call to org/egothor/methodatlas/command/CredentialDetectionRunner::logSummary → NO_COVERAGE

250

1.1
Location : emitFindings
Killed by : none
removed call to org/egothor/methodatlas/command/CredentialDetectionRunner::logFindings → NO_COVERAGE

251

1.1
Location : emitFindings
Killed by : none
removed call to org/egothor/methodatlas/command/CredentialDetectionRunner::recordIntoSarif → NO_COVERAGE

252

1.1
Location : emitFindings
Killed by : none
removed call to org/egothor/methodatlas/command/CredentialDetectionRunner::writeCsv → NO_COVERAGE

262

1.1
Location : discoverByFile
Killed by : none
replaced return value with Collections.emptyMap for org/egothor/methodatlas/command/CredentialDetectionRunner::discoverByFile → NO_COVERAGE

264

1.1
Location : discoverByFile
Killed by : none
removed call to org/egothor/methodatlas/command/PluginLoader::closeAll → NO_COVERAGE

271

1.1
Location : toAttribution
Killed by : none
removed call to java/util/Map::forEach → NO_COVERAGE

272

1.1
Location : lambda$toAttribution$4
Killed by : none
replaced return value with null for org/egothor/methodatlas/command/CredentialDetectionRunner::lambda$toAttribution$4 → NO_COVERAGE

274

1.1
Location : toAttribution
Killed by : none
replaced return value with Collections.emptyMap for org/egothor/methodatlas/command/CredentialDetectionRunner::toAttribution → NO_COVERAGE

279

1.1
Location : selectUnits
Killed by : none
removed conditional - replaced equality check with false → NO_COVERAGE

2.2
Location : selectUnits
Killed by : none
removed conditional - replaced equality check with true → NO_COVERAGE

280

1.1
Location : selectUnits
Killed by : none
replaced return value with Collections.emptyList for org/egothor/methodatlas/command/CredentialDetectionRunner::selectUnits → NO_COVERAGE

282

1.1
Location : selectUnits
Killed by : none
replaced return value with Collections.emptyList for org/egothor/methodatlas/command/CredentialDetectionRunner::selectUnits → NO_COVERAGE

283

1.1
Location : lambda$selectUnits$6
Killed by : none
replaced return value with null for org/egothor/methodatlas/command/CredentialDetectionRunner::lambda$selectUnits$6 → NO_COVERAGE

289

1.1
Location : toUnit
Killed by : none
removed conditional - replaced equality check with false → NO_COVERAGE

2.2
Location : toUnit
Killed by : none
removed conditional - replaced equality check with true → NO_COVERAGE

295

1.1
Location : toUnit
Killed by : none
replaced return value with null for org/egothor/methodatlas/command/CredentialDetectionRunner::toUnit → NO_COVERAGE

309

1.1
Location : runDetectors
Killed by : none
replaced return value with Collections.emptyList for org/egothor/methodatlas/command/CredentialDetectionRunner::runDetectors → NO_COVERAGE

311

1.1
Location : runDetectors
Killed by : none
removed call to org/egothor/methodatlas/command/PluginLoader::closeAllCredentialDetectors → NO_COVERAGE

316

1.1
Location : groupByFqcn
Killed by : none
replaced return value with Collections.emptyMap for org/egothor/methodatlas/command/CredentialDetectionRunner::groupByFqcn → NO_COVERAGE

317

1.1
Location : lambda$groupByFqcn$8
Killed by : none
removed conditional - replaced equality check with false → NO_COVERAGE

2.2
Location : lambda$groupByFqcn$8
Killed by : none
removed conditional - replaced equality check with true → NO_COVERAGE

3.3
Location : lambda$groupByFqcn$8
Killed by : none
replaced return value with "" for org/egothor/methodatlas/command/CredentialDetectionRunner::lambda$groupByFqcn$8 → NO_COVERAGE

323

1.1
Location : candidatesByFqcn
Killed by : none
removed call to java/util/Map::forEach → NO_COVERAGE

324

1.1
Location : lambda$candidatesByFqcn$10
Killed by : none
removed conditional - replaced equality check with true → NO_COVERAGE

2.2
Location : lambda$candidatesByFqcn$10
Killed by : none
removed conditional - replaced equality check with false → NO_COVERAGE

326

1.1
Location : lambda$candidatesByFqcn$9
Killed by : none
replaced return value with null for org/egothor/methodatlas/command/CredentialDetectionRunner::lambda$candidatesByFqcn$9 → NO_COVERAGE

331

1.1
Location : candidatesByFqcn
Killed by : none
replaced return value with Collections.emptyMap for org/egothor/methodatlas/command/CredentialDetectionRunner::candidatesByFqcn → NO_COVERAGE

335

1.1
Location : triageSeparately
Killed by : none
removed conditional - replaced equality check with true → NO_COVERAGE

2.2
Location : triageSeparately
Killed by : none
removed conditional - replaced equality check with false → NO_COVERAGE

336

1.1
Location : triageSeparately
Killed by : none
replaced return value with Collections.emptyList for org/egothor/methodatlas/command/CredentialDetectionRunner::triageSeparately → NO_COVERAGE

341

1.1
Location : triageSeparately
Killed by : none
removed call to java/util/Map::forEach → NO_COVERAGE

343

1.1
Location : triageSeparately
Killed by : none
replaced return value with Collections.emptyList for org/egothor/methodatlas/command/CredentialDetectionRunner::triageSeparately → NO_COVERAGE

347

1.1
Location : triageGroup
Killed by : none
removed conditional - replaced equality check with false → NO_COVERAGE

2.2
Location : triageGroup
Killed by : none
removed conditional - replaced equality check with true → NO_COVERAGE

351

1.1
Location : lambda$triageGroup$12
Killed by : none
replaced return value with null for org/egothor/methodatlas/command/CredentialDetectionRunner::lambda$triageGroup$12 → NO_COVERAGE

357

1.1
Location : lambda$triageGroup$14
Killed by : none
replaced return value with null for org/egothor/methodatlas/command/CredentialDetectionRunner::lambda$triageGroup$14 → NO_COVERAGE

2.2
Location : lambda$triageGroup$13
Killed by : none
replaced return value with null for org/egothor/methodatlas/command/CredentialDetectionRunner::lambda$triageGroup$13 → NO_COVERAGE

358

1.1
Location : triageGroup
Killed by : none
replaced return value with Collections.emptyList for org/egothor/methodatlas/command/CredentialDetectionRunner::triageGroup → NO_COVERAGE

361

1.1
Location : lambda$triageGroup$15
Killed by : none
replaced return value with "" for org/egothor/methodatlas/command/CredentialDetectionRunner::lambda$triageGroup$15 → NO_COVERAGE

362

1.1
Location : triageGroup
Killed by : none
replaced return value with Collections.emptyList for org/egothor/methodatlas/command/CredentialDetectionRunner::triageGroup → NO_COVERAGE

370

1.1
Location : filterByMinScore
Killed by : none
removed conditional - replaced comparison check with true → NO_COVERAGE

2.2
Location : filterByMinScore
Killed by : none
removed conditional - replaced equality check with false → NO_COVERAGE

3.3
Location : filterByMinScore
Killed by : none
removed conditional - replaced comparison check with false → NO_COVERAGE

4.4
Location : filterByMinScore
Killed by : none
changed conditional boundary → NO_COVERAGE

5.5
Location : filterByMinScore
Killed by : none
removed conditional - replaced equality check with true → NO_COVERAGE

374

1.1
Location : filterByMinScore
Killed by : none
replaced return value with Collections.emptyList for org/egothor/methodatlas/command/CredentialDetectionRunner::filterByMinScore → NO_COVERAGE

388

1.1
Location : logFindings
Killed by : none
removed conditional - replaced equality check with false → NO_COVERAGE

2.2
Location : logFindings
Killed by : none
removed conditional - replaced equality check with true → NO_COVERAGE

397

1.1
Location : recordIntoSarif
Killed by : none
removed conditional - replaced equality check with true → NO_COVERAGE

2.2
Location : recordIntoSarif
Killed by : none
removed conditional - replaced equality check with false → NO_COVERAGE

401

1.1
Location : recordIntoSarif
Killed by : none
removed call to org/egothor/methodatlas/emit/SarifEmitter::recordSecret → NO_COVERAGE

406

1.1
Location : fileUri
Killed by : none
removed conditional - replaced equality check with true → NO_COVERAGE

2.2
Location : fileUri
Killed by : none
removed conditional - replaced equality check with false → NO_COVERAGE

407

1.1
Location : fileUri
Killed by : none
replaced return value with "" for org/egothor/methodatlas/command/CredentialDetectionRunner::fileUri → NO_COVERAGE

409

1.1
Location : fileUri
Killed by : none
replaced return value with "" for org/egothor/methodatlas/command/CredentialDetectionRunner::fileUri → NO_COVERAGE

414

1.1
Location : writeCsv
Killed by : none
removed call to org/egothor/methodatlas/emit/CredentialCsvEmitter::flush → NO_COVERAGE

Active mutators

Tests examined


Report generated by PIT 1.22.1