///|
const VERSION : String = "0.4.0"
///|
fn print_help() -> Unit {
println(
(
#|MoonSARIF 0.4.0
#|
#|Pure MoonBit SARIF 2.1.0 toolkit.
#|
#|Usage:
#| moonsarif [arguments] [options]
#|
#|Commands:
#| validate [options] Validate a SARIF log
#| github-check Check GitHub Code Scanning compatibility
#| summary Print aggregate result counts
#| filter [options] Keep matching results and print SARIF
#| merge ... [options] Concatenate compatible SARIF logs
#| deduplicate [options] Remove duplicate findings
#| baseline Compare current results with a baseline
#| report [options] Render a Markdown or HTML report
#| version Show the current version
#| help Show this message
#|
#|Options:
#| --level Filter by error/warning/note/none
#| --rule Filter by rule id
#| --path Filter by normalized artifact path
#| --output Write output to a file instead of stdout
#| --pretty Pretty-print JSON output
#| --format Report output format (default: markdown)
#| --profile Validation profile for validate
#| --baseline Annotate report findings against a baseline
#| --fail-on-new Fail baseline checks when new findings exist
#| --max-new Allow at most this many new findings
#|
#|Exit codes:
#| 0 success (warnings do not fail validation)
#| 1 validation found SARIF errors
#| 2 command or input error
#| 3 baseline gate rejected new findings
#|
),
)
}
///|
fn print_version() -> Unit {
println("MoonSARIF \{VERSION}")
}
///|
fn option_value(args : ArrayView[String], names : ArrayView[String]) -> String? {
for index, arg in args {
if names.any(fn(name) { arg == name }) && index + 1 < args.length() {
return Some(args[index + 1])
}
}
None
}
///|
fn has_option(args : ArrayView[String], name : String) -> Bool {
args.any(fn(arg) { arg == name })
}
///|
fn int_option(args : ArrayView[String], name : String) -> Int? {
match option_value(args, [name]) {
Some(value) => Some(@string.parse_int(value)) catch { _ => None }
None => None
}
}
///|
fn positional_args(args : ArrayView[String]) -> Array[String] {
let values : Array[String] = []
let mut skip_next = false
for _, arg in args {
if skip_next {
skip_next = false
} else if arg == "--level" ||
arg == "--rule" ||
arg == "--rule-id" ||
arg == "--path" ||
arg == "--output" ||
arg == "--format" ||
arg == "--profile" ||
arg == "--baseline" ||
arg == "--max-new" {
skip_next = true
} else if !arg.has_prefix("--") {
values.push(arg)
}
}
values
}
///|
fn load_log(path : String) -> @moonsarif.SarifLog? {
try {
let content = @fs.read_file_to_string(path)
Some(@moonsarif.parse(content))
} catch {
_ => {
println("error: unable to read or parse SARIF file '\{path}'")
None
}
}
}
///|
fn write_output(content : String, output : String?) -> Bool {
match output {
None => {
println(content)
true
}
Some(path) =>
try {
@fs.write_string_to_file(path, content)
println("wrote \{path}")
true
} catch {
_ => {
println("error: unable to write output file '\{path}'")
false
}
}
}
}
///|
fn parse_profile(value : String) -> @moonsarif.ValidationProfile? {
@moonsarif.parse_validation_profile(value)
}
///|
fn command_validate(args : ArrayView[String]) -> Int {
let paths = positional_args(args)
guard paths is [path] else {
println("error: validate expects exactly one SARIF file")
print_help()
return 2
}
guard load_log(path) is Some(log) else { return 2 }
let profile = match option_value(args, ["--profile"]) {
Some(value) =>
match parse_profile(value) {
Some(profile) => profile
None => {
println("error: --profile must be generic, github, or strict")
return 2
}
}
None => @moonsarif.generic_validation_profile()
}
let report = @moonsarif.validate_with_profile(log, profile)
let output = report.to_json().stringify(indent=2)
if !write_output(output, option_value(args, ["--output"])) {
return 2
}
if report.is_valid() {
0
} else {
1
}
}
///|
fn command_github_check(args : ArrayView[String]) -> Int {
let paths = positional_args(args)
guard paths is [path] else {
println("error: github-check expects exactly one SARIF file")
print_help()
return 2
}
guard load_log(path) is Some(log) else { return 2 }
let report = @moonsarif.validate_github_compatibility(log)
let output = report.to_json().stringify(indent=2)
if !write_output(output, option_value(args, ["--output"])) {
return 2
}
if report.is_valid() {
0
} else {
1
}
}
///|
fn command_summary(args : ArrayView[String]) -> Int {
let paths = positional_args(args)
guard paths is [path] else {
println("error: summary expects exactly one SARIF file")
print_help()
return 2
}
guard load_log(path) is Some(log) else { return 2 }
let summary = @moonsarif.summarize(log)
let pretty = has_option(args, "--pretty")
let output = summary.to_json().stringify(indent=if pretty { 2 } else { 0 })
if write_output(output, option_value(args, ["--output"])) {
0
} else {
2
}
}
///|
fn command_filter(args : ArrayView[String]) -> Int {
let paths = positional_args(args)
guard paths is [path] else {
println("error: filter expects exactly one input SARIF file")
print_help()
return 2
}
guard load_log(path) is Some(log) else { return 2 }
let filtered = @moonsarif.filter_results(
log,
level?=option_value(args, ["--level"]),
rule_id?=option_value(args, ["--rule", "--rule-id"]),
path_contains?=option_value(args, ["--path"]),
)
let pretty = has_option(args, "--pretty") ||
option_value(args, ["--output"]) is Some(_)
let output = @moonsarif.stringify(filtered, pretty~)
if write_output(output, option_value(args, ["--output"])) {
0
} else {
2
}
}
///|
fn command_merge(args : ArrayView[String]) -> Int {
let paths = positional_args(args)
guard !paths.is_empty() else {
println("error: merge expects at least one input SARIF file")
print_help()
return 2
}
let logs : Array[@moonsarif.SarifLog] = []
for path in paths {
guard load_log(path) is Some(log) else { return 2 }
logs.push(log)
}
guard @moonsarif.merge(logs) is Some(merged) else {
println("error: cannot merge SARIF logs with different versions")
return 2
}
let pretty = has_option(args, "--pretty") ||
option_value(args, ["--output"]) is Some(_)
let output = @moonsarif.stringify(merged, pretty~)
if write_output(output, option_value(args, ["--output"])) {
0
} else {
2
}
}
///|
///|
fn command_deduplicate(args : ArrayView[String]) -> Int {
let paths = positional_args(args)
guard paths is [path] else {
println("error: deduplicate expects exactly one input SARIF file")
print_help()
return 2
}
guard load_log(path) is Some(log) else { return 2 }
let output = @moonsarif.stringify(@moonsarif.deduplicate(log), pretty=true)
if write_output(output, option_value(args, ["--output"])) {
0
} else {
2
}
}
///|
fn command_baseline(args : ArrayView[String]) -> Int {
let paths = positional_args(args)
guard paths is [current_path, baseline_path] else {
println("error: baseline expects a current log and a baseline log")
print_help()
return 2
}
guard load_log(current_path) is Some(current) else { return 2 }
guard load_log(baseline_path) is Some(baseline) else { return 2 }
let report = @moonsarif.compare_baseline(current, baseline)
let output = report.to_json().stringify(indent=2)
if !write_output(output, option_value(args, ["--output"])) {
return 2
}
let max_new = int_option(args, "--max-new")
if has_option(args, "--max-new") && max_new is None {
println("error: --max-new expects a non-negative integer")
return 2
}
if max_new is Some(limit) && (limit < 0 || report.new_results > limit) {
return 3
}
if has_option(args, "--fail-on-new") && report.new_results > 0 {
return 3
}
0
}
///|
fn command_report(args : ArrayView[String]) -> Int {
let paths = positional_args(args)
guard paths is [path] else {
println("error: report expects exactly one input SARIF file")
print_help()
return 2
}
guard load_log(path) is Some(log) else { return 2 }
let log = match option_value(args, ["--baseline"]) {
Some(baseline_path) =>
match load_log(baseline_path) {
Some(baseline) => @moonsarif.annotate_baseline(log, baseline)
None => return 2
}
None => log
}
let format = match option_value(args, ["--format"]) {
Some(value) => value
None => "markdown"
}
let output = match format {
"markdown" => @moonsarif.render_markdown(log)
"html" => @moonsarif.render_html(log)
_ => {
println("error: --format must be markdown or html")
return 2
}
}
if write_output(output, option_value(args, ["--output"])) {
0
} else {
2
}
}
///|
fn main {
let args = @env.args()
let exit_code = match args {
[_, "version", ..] => {
print_version()
0
}
[_, "help", ..] | [_] => {
print_help()
0
}
[_, "validate", .. rest] => command_validate(rest)
[_, "github-check", .. rest] => command_github_check(rest)
[_, "summary", .. rest] => command_summary(rest)
[_, "filter", .. rest] => command_filter(rest)
[_, "merge", .. rest] => command_merge(rest)
[_, "deduplicate", .. rest] => command_deduplicate(rest)
[_, "baseline", .. rest] => command_baseline(rest)
[_, "report", .. rest] => command_report(rest)
[_, command, ..] => {
println("error: unknown command '\{command}'")
print_help()
2
}
[] => {
print_help()
0
}
}
@sys.exit(exit_code)
}