-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathBaseReporter.cs
More file actions
359 lines (309 loc) · 15.1 KB
/
Copy pathBaseReporter.cs
File metadata and controls
359 lines (309 loc) · 15.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
namespace SCAScanner;
/// <summary>
/// Abstract base reporter. Contains all print logic; subclasses provide
/// Write/WriteLine implementations (with or without color).
/// </summary>
public abstract class BaseReporter : IReporter
{
protected OutputLevel Level { get; }
protected BaseReporter(OutputLevel level = OutputLevel.Standard)
{
Level = level;
}
protected abstract void Write(string text, ConsoleColor? color = null);
protected abstract void WriteLine(string text = "", ConsoleColor? color = null);
protected void WritePass(string text) => Write(text, ConsoleColor.Green);
protected void WriteFail(string text) => Write(text, ConsoleColor.Red);
protected void WriteInvalid(string text) => Write(text, ConsoleColor.DarkYellow);
protected void WriteLabel(string text) => Write(text, ConsoleColor.DarkCyan);
protected void WriteGray(string text) => Write(text, ConsoleColor.DarkGray);
protected static string Truncate(string text, int maxLength) => StringUtils.Truncate(text, maxLength);
// =========================================================================
// UI Structure
// =========================================================================
public void PrintBanner()
{
WriteLine("╔═════════════════════════════════════════════╗", ConsoleColor.Cyan);
WriteLine("║ Wazuh SCA Policy Scanner ║", ConsoleColor.Cyan);
WriteLine("╚═════════════════════════════════════════════╝", ConsoleColor.Cyan);
WriteLine();
}
public void PrintHelp()
{
PrintBanner();
WriteLine("USAGE:", ConsoleColor.White);
WriteLine(" SCAScanner [options] <path/to/policy.yaml> Run all checks from a policy file");
WriteLine(" SCAScanner [options] <path/to/dir> Scan all .yml/.yaml policies in a directory,");
WriteLine(" skipping those whose requirements don't apply");
WriteLine();
WriteLine("OPTIONS:", ConsoleColor.White);
WriteLine(" --display-details Show full rule details in console output");
WriteLine(" --no-details Show only header and summary (no requirements or rules)");
WriteLine(" --output-dir <dir> Root for default files (default: REPORTS_DIR or ./results/)");
WriteLine(" -l, --log <file> Write detailed output to a log file");
WriteLine(" --csv <file> Write scan results as CSV (one row per check)");
WriteLine(" -r, --report <file> Write scan results in SCAP-SCC log format");
WriteLine(" --sbom-file <file> Write Trivy CycloneDX SBOM to file");
WriteLine(" --sbom-target <path> Set Trivy SBOM target path (default: system drive root on Windows, / elsewhere)");
WriteLine(" --sbom-all-drives On Windows, write one SBOM per ready fixed local drive");
WriteLine(" --sbom-timeout <duration> Set Trivy timeout (e.g., 5m, 30s, 1h, none)");
WriteLine(" --sbom-skip-dir <list> Comma-separated directories to skip in SBOM scan");
WriteLine(" --sbom-skip-file <list> Comma-separated files to skip in SBOM scan");
WriteLine();
WriteLine("SFTP UPLOAD OPTIONS:", ConsoleColor.White);
WriteLine(" --sftp <host[:port]> Upload generated files to SFTP server");
WriteLine(" --sftp-user <user> SFTP username (env: SFTP_USER)");
WriteLine(" --sftp-pass <pass> SFTP password (env: SFTP_PASS) - ignored if using key auth");
WriteLine(" --sftp-key <path> SSH private key file path for key-based authentication");
WriteLine(" --sftp-path <path> Remote directory path (env: SFTP_PATH, default: /)");
WriteLine();
WriteLine("CONFIG FILE OPTIONS:", ConsoleColor.White);
WriteLine(" -c, --config <path> Load configuration from YAML file");
WriteLine(" --write-config [path] Generate a template config file (default: config.yml)");
WriteLine(" Use with path to write to custom location");
WriteLine();
WriteLine(" -h, --help Show this help message");
WriteLine();
WriteLine("NOTES:", ConsoleColor.White);
WriteLine(" - Config file is optional. By default, app looks for 'config.yml' in working dir");
WriteLine(" - CLI arguments always override config file values");
WriteLine(" - Default reports are written to REPORTS_DIR when set, otherwise ./results/");
WriteLine(" - SBOM generation runs on every scan using Trivy from 'SCA_SCANNER/bin'");
WriteLine(" - SBOM output lists detected software components, not every file on disk");
WriteLine(" - Successful Trivy warnings are saved next to the SBOM as *.trivy.log");
WriteLine(" - Trivy timeout defaults to 5m; use --sbom-timeout none to disable");
WriteLine();
WriteLine("EXAMPLES:", ConsoleColor.White);
WriteLine(" SCAScanner Policies/sample_policy.yaml");
WriteLine(" SCAScanner --display-details Policies/sample_policy.yaml");
WriteLine(" SCAScanner --write-config");
WriteLine(" SCAScanner --config custom.yml --no-details --csv report.csv Policies/");
WriteLine(" SCAScanner --sbom-file host-sbom.cdx.json Policies/sample_policy.yaml");
WriteLine(" SCAScanner --sbom-all-drives --sbom-file host-sbom.cdx.json Policies/sample_policy.yaml");
WriteLine();
}
// =========================================================================
// Policy Execution Context
// =========================================================================
public void PrintPolicyHeader(SCAPolicy policy, string platform)
{
PrintBanner();
WriteLine($" Policy : {policy.Policy.Name}");
WriteLine($" ID : {policy.Policy.Id}");
WriteLine($" Platform : {platform}");
WriteLine($" Checks : {policy.Checks.Count}");
if (!string.IsNullOrWhiteSpace(policy.Policy.Description))
WriteLine($" Desc : {policy.Policy.Description}");
WriteLine();
}
public void PrintRequirementsSection(Check requirementsCheck, PolicyVariables? variables, CheckResult requirementsResult)
{
if (Level == OutputLevel.Compact) return;
WriteLine("══ Requirements " + new string('═', 52), ConsoleColor.Cyan);
WriteLine($" {requirementsCheck.Title}");
if (!string.IsNullOrWhiteSpace(requirementsCheck.Description))
WriteLine($" {requirementsCheck.Description}");
WriteLabel(" ┌─ What requirements will be checked:\n");
List<ParsedRule> parsedReqRules = requirementsCheck.Rules
.Select(rule => RuleParser.Parse(rule, variables))
.ToList();
for (int i = 0; i < parsedReqRules.Count; i++)
{
WriteLabel($" │ [{i + 1}] ");
WriteLine(RuleParser.Explain(parsedReqRules[i]));
WriteGray($" │ raw : {requirementsCheck.Rules[i]}\n");
}
WriteLabel(" └─\n");
WriteLabel(" ┌─ Requirement results:\n");
for (int i = 0; i < requirementsResult.RuleResults.Count; i++)
{
RuleResult ruleResult = requirementsResult.RuleResults[i];
WriteLabel($" │ [{i + 1}] ");
if (ruleResult.Passed) WritePass("PASS"); else WriteFail("FAIL");
WriteLine($" {ruleResult.Detail}");
}
WriteLabel(" └─\n\n");
if (requirementsResult.Status != CheckStatus.Passed)
{
WriteFail(" ✗ Requirements NOT met — policy scan aborted.\n");
WriteFail($" {requirementsResult.Reason}\n");
}
else
{
WritePass(" ✓ Requirements met — proceeding with checks.\n");
WriteLine();
}
}
// =========================================================================
// Check Execution
// =========================================================================
public void PrintCheckHeader(Check check)
{
if (Level != OutputLevel.Detailed) return;
WriteLine(new string('─', 68));
Write($" [#{check.Id,-4}] ", ConsoleColor.DarkGray);
WriteLine(check.Title, ConsoleColor.Yellow);
if (!string.IsNullOrWhiteSpace(check.Description))
WriteLine($" Description : {check.Description}");
if (!string.IsNullOrWhiteSpace(check.Rationale))
WriteLine($" Rationale : {check.Rationale}", ConsoleColor.DarkGray);
if (!string.IsNullOrWhiteSpace(check.Remediation))
WriteLine($" Remediation : {check.Remediation}", ConsoleColor.DarkYellow);
WriteLine($" Condition : {check.Condition.ToUpper()} rules must pass", ConsoleColor.DarkGray);
WriteLine();
}
public void PrintRuleExplanations(List<ParsedRule> parsedRules, Check check, PolicyVariables? variables)
{
if (Level != OutputLevel.Detailed) return;
WriteLabel(" ┌─ What this check validates:\n");
for (int i = 0; i < parsedRules.Count; i++)
{
WriteLabel($" │ [{i + 1}] ");
WriteLine(RuleParser.Explain(parsedRules[i]));
WriteGray($" │ raw : {check.Rules[i]}\n");
if (variables?.HasVariables == true)
{
string expanded = check.Rules[i];
bool hasVar = false;
foreach (var (key, value) in variables.Values)
if (expanded.Contains(key)) { expanded = expanded.Replace(key, value); hasVar = true; }
if (hasVar)
WriteGray($" │ exp : {expanded}\n");
}
}
WriteLabel(" └─\n\n");
}
public void PrintRuleResults(IReadOnlyList<RuleResult> ruleResults)
{
if (Level != OutputLevel.Detailed) return;
WriteLabel(" ┌─ Executing:\n");
for (int i = 0; i < ruleResults.Count; i++)
{
RuleResult ruleResult = ruleResults[i];
WriteLabel($" │ [{i + 1}] ");
if (ruleResult.Invalid) WriteInvalid("INVALID");
else if (ruleResult.Passed) WritePass("PASS");
else WriteFail("FAIL");
WriteLine($" {ruleResult.Detail}");
}
}
public void PrintCheckResult(Check check, CheckResult result, int totalPassed, int totalFailed)
{
if (Level != OutputLevel.Detailed) return;
Write(" └─ ");
if (result.Status == CheckStatus.Passed)
{
WritePass("✓ PASSED");
WriteLine($" ({result.Reason})");
}
else if (result.Status == CheckStatus.Failed)
{
WriteFail("✗ FAILED");
WriteLine($" ({result.Reason})");
}
else if (result.Status == CheckStatus.Invalid)
{
WriteInvalid("⚠ INVALID");
WriteLine($" ({result.Reason})");
}
WriteLine();
}
// =========================================================================
// Summary
// =========================================================================
public void PrintScanSummary(int passed, int failed, int invalid, List<ScanCheckResult> checkResults)
{
WriteLine(new string('═', 68));
WriteLine(" SCAN SUMMARY");
WriteLine(new string('═', 68));
int total = passed + failed + invalid;
int score = total > 0 ? (int)Math.Round(passed * 100.0 / total) : 0;
WritePass($" Passed : {passed}\n");
WriteFail($" Failed : {failed}\n");
WriteLine($" Invalid : {invalid}", ConsoleColor.DarkYellow);
WriteLine($" Total : {total}");
Write(" Score : ");
ConsoleColor scoreColor = score >= 75 ? ConsoleColor.Green
: score >= 50 ? ConsoleColor.Yellow
: ConsoleColor.Red;
WriteLine($"{score}%", scoreColor);
WriteLine();
if (Level != OutputLevel.Compact)
{
WriteLine(" CHECK STATUS:");
foreach (ScanCheckResult result in checkResults)
{
Write($" [{result.Id,-4}] ", ConsoleColor.DarkGray);
if (result.Status == CheckStatus.Passed)
WritePass("✓ PASS");
else if (result.Status == CheckStatus.Failed)
WriteFail("✗ FAIL");
else if (result.Status == CheckStatus.Invalid)
WriteInvalid("⚠ INVALID");
WriteLine($" {result.Title}");
}
WriteLine();
}
}
// =========================================================================
// Directory Scanning
// =========================================================================
public void PrintDiscoveryHeader(string directoryPath, int foundCount)
{
PrintBanner();
WriteLine(" POLICY DISCOVERY");
WriteLine($" Directory : {Path.GetFullPath(directoryPath)}");
WriteLine($" Found : {foundCount} policy file(s)");
WriteLine();
WriteLine(" Checking requirements...", ConsoleColor.DarkGray);
}
public void PrintRequirementCheckLine(string fileName, bool met, string? note)
{
if (met)
{
Write($" ✓ {fileName,-40}", ConsoleColor.Green);
WriteLine($" [{note}]", ConsoleColor.DarkGray);
}
else
{
Write($" ✗ {fileName,-40}", ConsoleColor.DarkGray);
WriteLine($" [{note}]", ConsoleColor.DarkGray);
}
}
public void PrintApplicablePoliciesLine(int applicableCount)
{
WriteLine();
WriteLine($" Running {applicableCount} applicable policy file(s)...");
WriteLine();
}
public void PrintPolicyExecutionHeader(int index, int total, string policyFileName)
{
WriteLine($"{'═',-3} [{index}/{total}] {policyFileName} " +
new string('═', Math.Max(0, 52 - policyFileName.Length)),
ConsoleColor.Cyan);
WriteLine();
}
public void PrintDirectoryScanComplete(int totalPolicies, int failedPolicies)
{
WriteLine();
}
// =========================================================================
// Error Messages
// =========================================================================
public void PrintError(string message)
{
WriteFail($"Error: {message}\n");
}
public void PrintWarning(string message)
{
WriteLine($"Warning: {message}", ConsoleColor.Yellow);
}
public void PrintInfo(string message)
{
WriteLine($"Info: {message}", ConsoleColor.Cyan);
}
public void PrintNoPolicesFound(string directoryPath)
{
WriteFail($" No .yml / .yaml files found in {directoryPath}\n");
}
}