YamlConfig.java

1
package org.egothor.methodatlas;
2
3
import java.io.IOException;
4
import java.nio.file.Path;
5
import java.util.List;
6
import java.util.Map;
7
import java.util.logging.Level;
8
import java.util.logging.Logger;
9
10
import com.fasterxml.jackson.annotation.JsonProperty;
11
import tools.jackson.core.JacksonException;
12
import tools.jackson.core.JsonParser;
13
import tools.jackson.databind.DeserializationContext;
14
import tools.jackson.databind.DeserializationFeature;
15
import tools.jackson.databind.ObjectMapper;
16
import tools.jackson.databind.ValueDeserializer;
17
import tools.jackson.databind.deser.DeserializationProblemHandler;
18
import tools.jackson.dataformat.yaml.YAMLMapper;
19
20
/**
21
 * Loads a YAML configuration file that provides default values for
22
 * command-line options.
23
 *
24
 * <p>
25
 * When a {@code -config <file>} argument is present, {@link CliArgs} calls
26
 * {@link #load(Path)} before processing the remaining arguments. The returned
27
 * {@link YamlConfigFile} seeds the initial values; any matching command-line
28
 * flag then overrides the YAML value.
29
 * </p>
30
 *
31
 * <h2>Supported fields</h2>
32
 *
33
 * <pre>
34
 * outputMode: csv          # csv | plain | sarif | json  (default: csv)
35
 * emitMetadata: false      # (default: false)
36
 * contentHash: false       # (default: false)
37
 * securityOnly: false      # (default: false)
38
 * includeNonSecurity: false  # opt-in: include non-security methods in SARIF output (default: false)
39
 * sarifOmitScores: false   # opt-out: omit interaction score / confidence from SARIF message text (default: false)
40
 * minConfidence: 0.0       # drop AI results below this threshold (requires ai.confidence: true; default: 0.0 = off)
41
 * driftDetect: false       # (default: false)
42
 * promoteAi: false         # RISKY, not recommended: -apply-tags-from-csv falls back to ai_tags/ai_display_name
43
 *                          # for methods whose curated tags/display_name are blank, writing UNVALIDATED AI
44
 *                          # output into source (default: false)
45
 * overrideFile: .methodatlas-overrides.yaml  # optional
46
 * fileSuffixes:
47
 *   - Test.java
48
 * testMarkers:             # annotation/attribute names; empty = provider defaults
49
 *   - Test
50
 *   - ParameterizedTest
51
 * properties:              # plugin-specific key/multi-value pairs (optional)
52
 *   functionNames:         # example: for a Jest/Mocha/Vitest TypeScript plugin
53
 *     - test
54
 *     - it
55
 * ai:
56
 *   enabled: true
57
 *   provider: ollama       # auto | ollama | openai | openrouter | anthropic | azure_openai | groq | xai | github_models | mistral
58
 *   model: qwen2.5-coder:7b
59
 *   baseUrl: http://localhost:11434
60
 *   apiKey: sk-...
61
 *   apiKeyEnv: MY_KEY_ENV
62
 *   taxonomyFile: /path/to/taxonomy.txt
63
 *   taxonomyMode: default  # default | optimized
64
 *   maxClassChars: 100000
65
 *   timeoutSec: 30
66
 *   maxRetries: 3
67
 *   confidence: false
68
 *   redactComments: false   # blank comments in the AI payload; aborts if a language has no redactor
69
 *   apiVersion: 2024-02-01 # Azure OpenAI REST API version (azure_openai only)
70
 * detectSecrets: false         # enable credential detection (default: false)
71
 * secretsInclude: "**&#47;*.java" # glob override for file mask (default: null = use fileSuffixes)
72
 * secretsRules: /path/to/rules.yaml  # custom rule catalog (default: null = built-in)
73
 * secretsOut: methodatlas-credentials.csv  # output path for secrets CSV (default: methodatlas-credentials.csv)
74
 * secretsSeparateLlm: false    # force standalone triage LLM call (default: false)
75
 * secretsShowValues: false     # print unmasked values (default: false)
76
 * secretsErrorThreshold: 0.8  # SARIF error score floor (default: 0.8)
77
 * secretsWarningThreshold: 0.4 # SARIF warning score floor (default: 0.4)
78
 * secretsMinScore: 0.0         # suppress findings below this score (default: 0.0 = keep all)
79
 * </pre>
80
 *
81
 * <p>
82
 * Unknown fields in the YAML file are tolerated (they never fail the load) but
83
 * each is logged at {@code WARNING} so operator typos are visible.
84
 * </p>
85
 *
86
 * @see CliArgs
87
 */
88
final class YamlConfig {
89
90
    private static final Logger LOG = Logger.getLogger(YamlConfig.class.getName());
91
92
    /**
93
     * Prevents instantiation of this utility class.
94
     */
95
    private YamlConfig() {
96
    }
97
98
    /**
99
     * Logs every unknown YAML key at {@code WARNING} so operator typos are
100
     * visible, then declines to handle it so Jackson skips the value (the mapper
101
     * has {@code FAIL_ON_UNKNOWN_PROPERTIES} disabled, preserving the
102
     * "unknown keys are tolerated" contract).
103
     */
104
    private static final class UnknownKeyLogger extends DeserializationProblemHandler {
105
        @Override
106
        public boolean handleUnknownProperty(DeserializationContext ctxt, JsonParser parser,
107
                ValueDeserializer<?> deserializer, Object beanOrClass, String propertyName) {
108
            if (LOG.isLoggable(Level.WARNING)) {
109 2 1. handleUnknownProperty : removed conditional - replaced equality check with false → SURVIVED
2. handleUnknownProperty : removed conditional - replaced equality check with true → KILLED
                Class<?> target = beanOrClass instanceof Class<?> c ? c : beanOrClass.getClass();
110
                LOG.log(Level.WARNING,
111
                        "Unknown configuration key ''{0}'' (in {1}) ignored — check for a typo.",
112
                        new Object[] { propertyName, target.getSimpleName() });
113
            }
114 1 1. handleUnknownProperty : replaced boolean return with true for org/egothor/methodatlas/YamlConfig$UnknownKeyLogger::handleUnknownProperty → SURVIVED
            return false; // not consumed here; Jackson skips it (FAIL_ON_UNKNOWN_PROPERTIES=false)
115
        }
116
    }
117
118
    /**
119
     * Loads a YAML configuration file.
120
     *
121
     * @param configFile path to the YAML file
122
     * @return parsed configuration; never {@code null}
123
     * @throws IOException              if the file cannot be read
124
     * @throws IllegalArgumentException if the file cannot be parsed as valid YAML
125
     */
126
    /* default */ static YamlConfigFile load(Path configFile) throws IOException {
127
        ObjectMapper mapper = YAMLMapper.builder()
128
                .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
129
                .addHandler(new UnknownKeyLogger())
130
                .build();
131
        try {
132 1 1. load : replaced return value with null for org/egothor/methodatlas/YamlConfig::load → KILLED
            return mapper.readValue(configFile.toFile(), YamlConfigFile.class);
133
        } catch (JacksonException e) {
134
            throw new IOException("Cannot read or parse configuration file '" + configFile + "'", e);
135
        }
136
    }
137
138
    // -------------------------------------------------------------------------
139
    // POJO classes
140
    // -------------------------------------------------------------------------
141
142
    /**
143
     * Top-level YAML configuration structure.
144
     *
145
     * <p>
146
     * Unknown keys are tolerated (the mapper disables
147
     * {@code FAIL_ON_UNKNOWN_PROPERTIES}) but each is logged at {@code WARNING}
148
     * by {@link UnknownKeyLogger}; hence no class-level
149
     * {@code @JsonIgnoreProperties(ignoreUnknown = true)} — that would silently
150
     * drop unknowns before the handler could see them.
151
     * </p>
152
     */
153
    @SuppressWarnings("PMD.TooManyFields")
154
    /* default */ static final class YamlConfigFile {
155
156
        /** Output mode: {@code csv}, {@code plain}, or {@code sarif}. */
157
        @JsonProperty("outputMode")
158
        /* default */ String outputMode;
159
160
        /** Whether to emit {@code # key: value} metadata comment lines. */
161
        @JsonProperty("emitMetadata")
162
        /* default */ boolean emitMetadata;
163
164
        /** File name suffixes used to select test source files. */
165
        @JsonProperty("fileSuffixes")
166
        /* default */ List<String> fileSuffixes;
167
168
        /**
169
         * Language-neutral test-marker identifiers (annotation/attribute simple
170
         * names for JVM and .NET providers; ignored by TypeScript providers).
171
         * Empty or absent means "use provider defaults".
172
         */
173
        @JsonProperty("testMarkers")
174
        /* default */ List<String> testMarkers;
175
176
        /**
177
         * Plugin-specific key/multi-value pairs forwarded verbatim to each
178
         * {@link org.egothor.methodatlas.api.TestDiscovery} provider.
179
         * Providers ignore keys they do not recognise.
180
         */
181
        @JsonProperty("properties")
182
        /* default */ Map<String, List<String>> properties;
183
184
        /**
185
         * Whether to include a SHA-256 content-hash fingerprint of each class
186
         * source as a {@code content_hash} column.
187
         */
188
        @JsonProperty("contentHash")
189
        /* default */ boolean contentHash;
190
191
        /**
192
         * Path to a YAML classification override file. When set, human-authored
193
         * corrections are applied after AI classification on every run.
194
         */
195
        @JsonProperty("overrideFile")
196
        /* default */ String overrideFile;
197
198
        /**
199
         * When {@code true}, only security-relevant methods are emitted; all
200
         * other methods are silently dropped from the output.
201
         */
202
        @JsonProperty("securityOnly")
203
        /* default */ boolean securityOnly;
204
205
        /**
206
         * When {@code true}, non-security methods are included in SARIF output
207
         * even though SARIF mode applies the security-only filter by default.
208
         * Has no effect in CSV or plain-text modes.
209
         */
210
        @JsonProperty("includeNonSecurity")
211
        /* default */ boolean includeNonSecurity;
212
213
        /**
214
         * When {@code true}, a {@code tag_ai_drift} column is added to CSV/plain
215
         * output comparing the source-level {@code @Tag("security")} annotation
216
         * against the AI security-relevance classification.
217
         */
218
        @JsonProperty("driftDetect")
219
        /* default */ boolean driftDetect;
220
221
        /**
222
         * <strong>Risky, not recommended.</strong> When {@code true}, the
223
         * {@code -apply-tags-from-csv} engine falls back to the {@code ai_tags}
224
         * and {@code ai_display_name} columns for any method whose curated
225
         * {@code tags} / {@code display_name} column is blank, writing the raw,
226
         * unvalidated AI suggestion into source. This bypasses the human review
227
         * step the apply-from-csv workflow exists to enforce. Default:
228
         * {@code false}.
229
         */
230
        @JsonProperty("promoteAi")
231
        /* default */ boolean promoteAi;
232
233
        /**
234
         * When {@code true}, the interaction score and confidence percentage are
235
         * omitted from SARIF result message text. Use this when the consuming
236
         * system already renders the {@code properties} bag and the extra text is
237
         * unwanted. Default: {@code false} (scores are embedded in messages).
238
         */
239
        @JsonProperty("sarifOmitScores")
240
        /* default */ boolean sarifOmitScores;
241
242
        /**
243
         * Minimum AI confidence score (inclusive) required for a method to be
244
         * emitted. Methods whose {@code ai_confidence} is below this threshold
245
         * are silently dropped. Only meaningful when {@code ai.confidence: true}
246
         * is also set. Default: {@code 0.0} (no filtering).
247
         */
248
        @JsonProperty("minConfidence")
249
        /* default */ Double minConfidence;
250
251
        /**
252
         * When {@code true}, enable credential detection in addition to the normal
253
         * test-method scan. Default: {@code false}.
254
         */
255
        @JsonProperty("detectSecrets")
256
        /* default */ boolean detectSecrets;
257
258
        /**
259
         * Glob pattern overriding the default test-file mask when scanning for
260
         * secrets. {@code null} means use the default mask derived from
261
         * {@code fileSuffixes}.
262
         */
263
        @JsonProperty("secretsInclude")
264
        /* default */ String secretsInclude;
265
266
        /**
267
         * Path to a custom rule catalog YAML file. {@code null} uses the built-in
268
         * catalog bundled with the detect-secrets module.
269
         */
270
        @JsonProperty("secretsRules")
271
        /* default */ String secretsRules;
272
273
        /**
274
         * Output path for the secrets CSV. {@code null} causes the default
275
         * {@code methodatlas-credentials.csv} in the current working directory to be
276
         * used.
277
         */
278
        @JsonProperty("secretsOut")
279
        /* default */ String secretsOut;
280
281
        /**
282
         * When {@code true}, force a standalone triage LLM call instead of
283
         * appending the secret-triage prompt to the normal test-classification
284
         * call. Default: {@code false}.
285
         */
286
        @JsonProperty("secretsSeparateLlm")
287
        /* default */ boolean secretsSeparateLlm;
288
289
        /**
290
         * When {@code true}, print unmasked secret values in CSV and SARIF output.
291
         * Default: {@code false} (values are redacted).
292
         */
293
        @JsonProperty("secretsShowValues")
294
        /* default */ boolean secretsShowValues;
295
296
        /**
297
         * SARIF error score floor. Findings at or above this value are emitted as
298
         * {@code error}-level SARIF results. Default: {@code 0.8}.
299
         */
300
        @JsonProperty("secretsErrorThreshold")
301
        /* default */ Double secretsErrorThreshold;
302
303
        /**
304
         * SARIF warning score floor. Findings at or above this value (but below
305
         * {@code secretsErrorThreshold}) are emitted as {@code warning}-level SARIF
306
         * results. Default: {@code 0.4}.
307
         */
308
        @JsonProperty("secretsWarningThreshold")
309
        /* default */ Double secretsWarningThreshold;
310
311
        /**
312
         * Suppress findings whose triage score is below this value. Default:
313
         * {@code 0.0} keeps all findings.
314
         */
315
        @JsonProperty("secretsMinScore")
316
        /* default */ Double secretsMinScore;
317
318
        /**
319
         * When {@code true}, AI classification calls for different classes are issued
320
         * concurrently using the common {@link java.util.concurrent.ForkJoinPool};
321
         * output remains in discovery order. Default: {@code false}.
322
         */
323
        @JsonProperty("parallelAi")
324
        /* default */ boolean parallelAi;
325
326
        /**
327
         * When {@code true}, append an {@code ai_cwe} column mapping AI taxonomy tags to CWE identifiers.
328
         * Default: {@code false}.
329
         */
330
        @JsonProperty("aiCwe")
331
        /* default */ boolean aiCwe;
332
333
        /**
334
         * When {@code true}, write a security-domain gap report JSON after the scan.
335
         * Default: {@code false}.
336
         */
337
        @JsonProperty("gapReport")
338
        /* default */ boolean gapReport;
339
340
        /**
341
         * Target path for the gap report file. Default: {@code security-gap-report.json}.
342
         * Only honoured when {@code gapReport} is {@code true}.
343
         */
344
        @JsonProperty("gapReportFile")
345
        /* default */ String gapReportFile;
346
347
        /**
348
         * When {@code true}, write a Markdown security evidence report after the scan.
349
         * Default: {@code false}.
350
         */
351
        @JsonProperty("evidenceReport")
352
        /* default */ boolean evidenceReport;
353
354
        /**
355
         * Target path for the evidence report file.
356
         * Default: {@code security-evidence-report.md} in the working directory.
357
         * Only honoured when {@code evidenceReport} is {@code true}.
358
         */
359
        @JsonProperty("evidenceReportFile")
360
        /* default */ String evidenceReportFile;
361
362
        /**
363
         * When {@code true}, write a JSON attestation manifest of security-relevant test
364
         * methods after the scan. Default: {@code false}.
365
         */
366
        @JsonProperty("attest")
367
        /* default */ boolean attest;
368
369
        /**
370
         * Target path for the attestation JSON manifest.
371
         * Default: {@code security-tests.attestation.json}. Only honoured when
372
         * {@code attest} is {@code true}.
373
         */
374
        @JsonProperty("attestFile")
375
        /* default */ String attestFile;
376
377
        /** AI enrichment settings. */
378
        @JsonProperty("ai")
379
        /* default */ YamlAiConfig ai;
380
    }
381
382
    /**
383
     * AI subsystem configuration within the YAML file. Unknown keys are logged
384
     * and skipped (see {@link YamlConfigFile}).
385
     */
386
    /* default */ static final class YamlAiConfig {
387
388
        /** Whether AI enrichment is enabled. */
389
        @JsonProperty("enabled")
390
        /* default */ Boolean enabled;
391
392
        /**
393
         * AI provider: {@code auto}, {@code ollama}, {@code openai},
394
         * {@code openrouter}, {@code anthropic}, {@code azure_openai},
395
         * {@code groq}, {@code xai}, {@code github_models}, or {@code mistral}.
396
         */
397
        @JsonProperty("provider")
398
        /* default */ String provider;
399
400
        /** Provider-specific model name. */
401
        @JsonProperty("model")
402
        /* default */ String model;
403
404
        /** Provider base URL override. */
405
        @JsonProperty("baseUrl")
406
        /* default */ String baseUrl;
407
408
        /** API key supplied directly. */
409
        @JsonProperty("apiKey")
410
        /* default */ String apiKey;
411
412
        /** Name of the environment variable that holds the API key. */
413
        @JsonProperty("apiKeyEnv")
414
        /* default */ String apiKeyEnv;
415
416
        /** Path to an external taxonomy file. */
417
        @JsonProperty("taxonomyFile")
418
        /* default */ String taxonomyFile;
419
420
        /** Built-in taxonomy variant: {@code default} or {@code optimized}. */
421
        @JsonProperty("taxonomyMode")
422
        /* default */ String taxonomyMode;
423
424
        /** Maximum number of characters of class source sent to the AI. */
425
        @JsonProperty("maxClassChars")
426
        /* default */ Integer maxClassChars;
427
428
        /** AI request timeout in seconds. */
429
        @JsonProperty("timeoutSec")
430
        /* default */ Long timeoutSec;
431
432
        /** Maximum number of retries for AI requests. */
433
        @JsonProperty("maxRetries")
434
        /* default */ Integer maxRetries;
435
436
        /** Whether to request a confidence score for each classification. */
437
        @JsonProperty("confidence")
438
        /* default */ Boolean confidence;
439
440
        /** Whether comments are blanked in the source sent to the AI provider (fail-closed). */
441
        @JsonProperty("redactComments")
442
        /* default */ Boolean redactComments;
443
444
        /**
445
         * Azure OpenAI REST API version appended as the {@code api-version} query
446
         * parameter; only used when {@code provider: azure_openai} is set.
447
         */
448
        @JsonProperty("apiVersion")
449
        /* default */ String apiVersion;
450
451
        /** Path to a custom method-classification prompt template (default: built-in). */
452
        @JsonProperty("classificationPrompt")
453
        /* default */ String classificationPrompt;
454
455
        /** Path to a custom folded credential-triage appendix template (default: built-in). */
456
        @JsonProperty("triagePrompt")
457
        /* default */ String triagePrompt;
458
459
        /** Path to a custom standalone credential-triage template (default: built-in). */
460
        @JsonProperty("dedicatedTriagePrompt")
461
        /* default */ String dedicatedTriagePrompt;
462
    }
463
}

Mutations

109

1.1
Location : handleUnknownProperty
Killed by : org.egothor.methodatlas.YamlConfigTest.[engine:junit-jupiter]/[class:org.egothor.methodatlas.YamlConfigTest]/[method:load_ignoresUnknownTopLevelFields(java.nio.file.Path)]
removed conditional - replaced equality check with true → KILLED

2.2
Location : handleUnknownProperty
Killed by : none
removed conditional - replaced equality check with false → SURVIVED
Covering tests

114

1.1
Location : handleUnknownProperty
Killed by : none
replaced boolean return with true for org/egothor/methodatlas/YamlConfig$UnknownKeyLogger::handleUnknownProperty → SURVIVED
Covering tests

132

1.1
Location : load
Killed by : org.egothor.methodatlas.YamlConfigTest.[engine:junit-jupiter]/[class:org.egothor.methodatlas.YamlConfigTest]/[method:load_parsesApiKeyFields(java.nio.file.Path)]
replaced return value with null for org/egothor/methodatlas/YamlConfig::load → KILLED

Active mutators

Tests examined


Report generated by PIT 1.30.1 support