namespace AttackTracerNinjaCli; internal sealed record ScanOptions { public const string Usage = """ Usage: OCSentinelCli [--output ] [--lookback-days ] [--top ] [--config ] [--vulnerability-csv ] [--json-only] [--ninja-output] [--fail-on-attacks] [--fail-on-threshold] [--help] Options: --output Write the JSON report to the given file. --lookback-days Only include events newer than now minus n days. Default: 30 --top Number of aggregated source IPs to show. Default: 10 --config Load thresholds, path overrides, and exclusions from JSON. --vulnerability-csv Correlate local attack results with exported CVE data for this host. --json-only Print only JSON to stdout. --ninja-output Print extra key=value lines for RMM/Ninja-style parsing. --fail-on-attacks Return exit code 1 when attacks are found. --fail-on-threshold Return exit code 1 when status is warning or critical. --help Show this message. """; public string? OutputPath { get; init; } public int LookbackDays { get; init; } = 30; public int TopCount { get; init; } = 10; public bool JsonOnly { get; init; } public bool NinjaOutput { get; init; } public bool FailOnAttacks { get; init; } public bool FailOnThreshold { get; init; } public string? ConfigPath { get; init; } public string? VulnerabilityCsvPath { get; init; } public bool ShowHelp { get; init; } public static ScanOptions Parse(string[] args) { var options = new ScanOptions(); for (int i = 0; i < args.Length; i++) { string arg = args[i]; switch (arg) { case "--help": case "-h": case "/?": options = options with { ShowHelp = true }; break; case "--json-only": options = options with { JsonOnly = true }; break; case "--ninja-output": options = options with { NinjaOutput = true }; break; case "--fail-on-attacks": options = options with { FailOnAttacks = true }; break; case "--fail-on-threshold": options = options with { FailOnThreshold = true }; break; case "--output": options = options with { OutputPath = ReadValue(args, ref i, arg) }; break; case "--config": options = options with { ConfigPath = ReadValue(args, ref i, arg) }; break; case "--vulnerability-csv": options = options with { VulnerabilityCsvPath = ReadValue(args, ref i, arg) }; break; case "--lookback-days": options = options with { LookbackDays = ReadPositiveInt(args, ref i, arg) }; break; case "--top": options = options with { TopCount = ReadPositiveInt(args, ref i, arg) }; break; default: throw new ArgumentException($"Unknown argument: {arg}"); } } return options; } private static string ReadValue(string[] args, ref int index, string argName) { if (index + 1 >= args.Length) { throw new ArgumentException($"Missing value for {argName}"); } index++; return args[index]; } private static int ReadPositiveInt(string[] args, ref int index, string argName) { string raw = ReadValue(args, ref index, argName); if (!int.TryParse(raw, out int value) || value <= 0) { throw new ArgumentException($"{argName} must be a positive integer"); } return value; } }