///|
fn main {
let cmd = @argparse.Command(
"moon-audit",
about="Static security scanner for MoonBit projects",
version="0.1.0",
flags=[
@argparse.FlagArg(
"quiet",
short='q',
long="quiet",
about="Only output findings",
),
@argparse.FlagArg(
"verbose",
short='v',
long="verbose",
about="Show detailed scan info",
),
@argparse.FlagArg(
"fail-on-error",
long="fail-on-error",
about="Exit with code 1 when error-level findings exist (for CI gating)",
),
],
options=[
@argparse.OptionArg(
"format",
short='f',
long="format",
about="Output format: text, json, sarif",
default_values=["text"],
),
@argparse.OptionArg(
"output",
short='o',
long="output",
about="Write output to file",
),
@argparse.OptionArg(
"config",
short='c',
long="config",
about="Config file path",
default_values=[".moon-audit.json"],
),
@argparse.OptionArg(
"severity",
long="severity",
about="Minimum severity: error, warning, info",
default_values=["warning"],
),
@argparse.OptionArg(
"rule",
long="rule",
about="Only run specific rule(s)",
),
@argparse.OptionArg(
"changed-files",
long="changed-files",
about="File containing changed file paths (one per line) for incremental scanning",
),
@argparse.OptionArg(
"baseline",
long="baseline",
about="Baseline file to exclude known findings",
),
],
positionals=[
@argparse.PositionArg("path", about="Project path to scan", default_values=[
".",
]),
],
subcommands=[
@argparse.Command("list-rules", about="List all available rules"),
@argparse.Command(
"llm-analyze",
about="Generate LLM analysis prompts for findings",
options=[
@argparse.OptionArg(
"format",
short='f',
long="format",
about="Output format: script (auto-run Python), json (API bodies), text (readable)",
default_values=["json"],
),
@argparse.OptionArg(
"output",
short='o',
long="output",
about="Write prompts to file",
),
@argparse.OptionArg(
"config",
short='c',
long="config",
about="Config file path",
default_values=[".moon-audit.json"],
),
],
positionals=[
@argparse.PositionArg("path", about="Project path to scan", default_values=[
".",
]),
],
),
@argparse.Command(
"generate-poc",
about="Generate PoC exploit scripts from findings",
options=[
@argparse.OptionArg(
"output",
short='o',
long="output",
about="Write PoC report to file",
),
@argparse.OptionArg(
"config",
short='c',
long="config",
about="Config file path",
default_values=[".moon-audit.json"],
),
],
positionals=[
@argparse.PositionArg("path", about="Project path to scan", default_values=[
".",
]),
],
),
@argparse.Command(
"remediate",
about="Generate remediation guide from findings",
options=[
@argparse.OptionArg(
"output",
short='o',
long="output",
about="Write remediation report to file",
),
@argparse.OptionArg(
"config",
short='c',
long="config",
about="Config file path",
default_values=[".moon-audit.json"],
),
],
positionals=[
@argparse.PositionArg("path", about="Project path to scan", default_values=[
".",
]),
],
),
@argparse.Command(
"pipeline",
about="Run analysis pipeline: scan + taint + summary",
flags=[
@argparse.FlagArg(
"fail-on-error",
long="fail-on-error",
about="Exit with code 1 when error-level findings exist",
),
],
options=[
@argparse.OptionArg(
"output-dir",
short='d',
long="output-dir",
about="Output directory for all reports (default: moon-audit-report)",
default_values=["moon-audit-report"],
),
@argparse.OptionArg(
"config",
short='c',
long="config",
about="Config file path",
default_values=[".moon-audit.json"],
),
],
positionals=[
@argparse.PositionArg("path", about="Project path to scan", default_values=[
".",
]),
],
),
@argparse.Command(
"summary",
about="Show scan summary with statistics",
options=[
@argparse.OptionArg(
"format",
short='f',
long="format",
about="Output format: text, json",
default_values=["text"],
),
@argparse.OptionArg(
"output",
short='o',
long="output",
about="Write summary to file",
),
@argparse.OptionArg(
"config",
short='c',
long="config",
about="Config file path",
default_values=[".moon-audit.json"],
),
],
positionals=[
@argparse.PositionArg("path", about="Project path to scan", default_values=[
".",
]),
],
),
@argparse.Command(
"generate-baseline",
about="Generate a baseline file from current scan findings",
options=[
@argparse.OptionArg(
"output",
short='o',
long="output",
about="Baseline output file path",
default_values=[".moon-audit-baseline.json"],
),
@argparse.OptionArg(
"config",
short='c',
long="config",
about="Config file path",
default_values=[".moon-audit.json"],
),
],
positionals=[
@argparse.PositionArg("path", about="Project path to scan", default_values=[
".",
]),
],
),
@argparse.Command(
"deep-audit",
about="Generate LLM-powered deep security audit script (static + AI code review)",
options=[
@argparse.OptionArg(
"output",
short='o',
long="output",
about="Output script file path",
default_values=["deep_audit.py"],
),
@argparse.OptionArg(
"config",
short='c',
long="config",
about="Config file path",
default_values=[".moon-audit.json"],
),
],
positionals=[
@argparse.PositionArg("path", about="Project path to scan", default_values=[
".",
]),
],
),
],
)
let matches = cmd.parse() catch {
e => {
println("Error: " + e.to_string())
println(cmd.render_help())
@sys.exit(2)
return
}
}
match matches.subcommand {
Some(("list-rules", _)) => cmd_list_rules()
Some(("llm-analyze", sub)) => cmd_llm_analyze(sub)
Some(("generate-poc", sub)) => cmd_generate_poc(sub)
Some(("remediate", sub)) => cmd_remediate(sub)
Some(("pipeline", sub)) => cmd_pipeline(sub)
Some(("summary", sub)) => cmd_summary(sub)
Some(("generate-baseline", sub)) => cmd_generate_baseline(sub)
Some(("deep-audit", sub)) => cmd_deep_audit(sub)
_ => cmd_scan(matches)
}
}
///|
fn cmd_list_rules() -> Unit {
let rules = @audit.list_all_rules()
let config = @audit.Config::default()
println("Available rules:\n")
for rule in rules {
let id = @audit.rule_id_str(rule.id)
let sev = @audit.severity_str(rule.default_severity)
let status = if config.is_enabled(id) { "on" } else { "off" }
println(" " + id + " [" + sev + "] (" + status + ")")
println(" " + rule.name)
println(" " + rule.description)
println("")
}
println("Rules marked (off) require project-level context and have higher")
println("false-positive risk. Enable them in .moon-audit.json when needed.")
}
///|
fn cmd_scan(matches : @argparse.Matches) -> Unit {
let path = match matches.values.get("path") {
Some(arr) => if arr.length() > 0 { arr[0] } else { "." }
None => "."
}
let config_path = match matches.values.get("config") {
Some(arr) => if arr.length() > 0 { arr[0] } else { ".moon-audit.json" }
None => ".moon-audit.json"
}
let format = match matches.values.get("format") {
Some(arr) => if arr.length() > 0 { arr[0] } else { "text" }
None => "text"
}
let output_file = match matches.values.get("output") {
Some(arr) => if arr.length() > 0 { Some(arr[0]) } else { None }
None => None
}
let verbose = match matches.flags.get("verbose") {
Some(v) => v
None => false
}
let quiet = match matches.flags.get("quiet") {
Some(v) => v
None => false
}
let fail_on_error = match matches.flags.get("fail-on-error") {
Some(v) => v
None => false
}
let severity_filter = match matches.values.get("severity") {
Some(arr) =>
if arr.length() > 0 {
@audit.severity_from_str(arr[0])
} else {
@audit.Warning
}
None => @audit.Warning
}
let resolved_config_path = if config_path == ".moon-audit.json" {
@path.Path(path).join(@path.Path(config_path)).0
} else {
config_path
}
let mut config = @audit.load_config(resolved_config_path)
config = { ..config, min_severity: severity_filter }
let changed_files_path = get_opt(matches, "changed-files")
match changed_files_path {
Some(cf_path) => {
let changed = @audit.load_changed_files(cf_path)
config = { ..config, changed_files: changed }
}
None => ()
}
let rule_filter = match matches.values.get("rule") {
Some(arr) => if arr.length() > 0 { Some(arr[0]) } else { None }
None => None
}
match rule_filter {
Some(rule_id) => {
let all_rules = @audit.list_all_rules()
for rule in all_rules {
let id = @audit.rule_id_str(rule.id)
if id != rule_id {
config.rules[id] = { enabled: false, severity: None }
}
}
}
None => ()
}
let path_ok = @fs.is_dir(path) catch { _ => false }
if !path_ok {
println("moon-audit: error: path not found or not a directory: " + path)
return
}
let machine_output = format == "json" || format == "sarif"
if !quiet && !machine_output {
println("moon-audit v0.2.1 - Static security scanner for MoonBit\n")
println("Scanning: " + path)
if config.changed_files.length() > 0 {
println(
"Incremental mode: " +
config.changed_files.length().to_string() +
" changed file(s)",
)
}
println("")
}
let result = @audit.scan_project(path, config)
let baseline_path = get_opt(matches, "baseline")
let baseline = match baseline_path {
Some(bp) => @audit.load_baseline(bp)
None => Map([])
}
let after_baseline = @audit.filter_baseline(result.findings, baseline)
let baseline_filtered = result.findings.length() - after_baseline.length()
if baseline_filtered > 0 && !quiet && !machine_output {
println(
"Baseline: filtered " +
baseline_filtered.to_string() +
" known finding(s)",
)
}
let filtered_findings = after_baseline.filter(fn(f) {
@audit.severity_level(f.severity) >= @audit.severity_level(severity_filter)
})
let filtered_result : @audit.ScanResult = {
findings: filtered_findings,
files_scanned: result.files_scanned,
errors: result.errors,
}
let output = if format == "json" {
@audit.format_json(filtered_result)
} else if format == "sarif" {
@audit.format_sarif(filtered_result)
} else {
@audit.format_text(filtered_result, verbose)
}
match output_file {
Some(out_path) => {
@fs.write_string_to_file(out_path, output) catch {
err => {
let _ = err
println("moon-audit: error: failed to write output file: " + out_path)
}
}
if !quiet && !machine_output {
println("Output written to: " + out_path)
}
}
None => println(output)
}
if result.errors.length() > 0 && !quiet && !machine_output {
println("")
println(
"Scan completed with " +
result.errors.length().to_string() +
" file error(s):",
)
for e in result.errors {
println(" " + e)
}
}
if fail_on_error {
let has_errors = filtered_findings
.iter()
.any(fn(f) { f.severity == @audit.Error })
if has_errors {
@sys.exit(1)
}
}
}
///|
fn get_val(
matches : @argparse.Matches,
key : String,
default : String,
) -> String {
match matches.values.get(key) {
Some(arr) => if arr.length() > 0 { arr[0] } else { default }
None => default
}
}
///|
fn get_opt(matches : @argparse.Matches, key : String) -> String? {
match matches.values.get(key) {
Some(arr) => if arr.length() > 0 { Some(arr[0]) } else { None }
None => None
}
}
///|
fn write_pipeline_file(path : String, content : String) -> Unit {
@fs.write_string_to_file(path, content) catch {
err => {
let _ = err
println("moon-audit: error: failed to write " + path)
}
}
}
///|
fn write_output(content : String, output_file : String?) -> Unit {
match output_file {
Some(out_path) => {
@fs.write_string_to_file(out_path, content) catch {
_ => println("Error writing output file")
}
println("Output written to: " + out_path)
}
None => println(content)
}
}
///|
fn do_scan(matches : @argparse.Matches) -> @audit.ScanResult? {
let path = get_val(matches, "path", ".")
let path_ok = @fs.is_dir(path) catch { _ => false }
if !path_ok {
println("moon-audit: error: path not found or not a directory: " + path)
return None
}
let config_path = get_val(matches, "config", ".moon-audit.json")
let resolved_config = if config_path == ".moon-audit.json" {
@path.Path(path).join(@path.Path(config_path)).0
} else {
config_path
}
let config = @audit.load_config(resolved_config)
Some(@audit.scan_project(path, config))
}
///|
fn cmd_llm_analyze(matches : @argparse.Matches) -> Unit {
let format = get_val(matches, "format", "json")
let output_file = get_opt(matches, "output")
let path = get_val(matches, "path", ".")
println("moon-audit LLM Analysis\n")
println("Scanning: " + path + "\n")
let result = match do_scan(matches) {
Some(r) => r
None => return
}
if result.findings.length() == 0 {
println("No findings to analyze.")
return
}
println(
"Found " +
result.findings.length().to_string() +
" finding(s), preparing LLM analysis...\n",
)
let llm_config = @audit.load_llm_config(path)
println("LLM Config:")
println(" Base URL: " + llm_config.base_url)
println(" Model: " + llm_config.model)
if llm_config.api_key.length() > 0 {
let end = if llm_config.api_key.length() > 8 {
8
} else {
llm_config.api_key.length()
}
let masked = llm_config.api_key[0:end].to_owned()
println(" API Key: " + masked + "...")
} else {
println(" API Key: (not set - configure LLM_API_KEY in .env)")
}
println("")
let sources : Map[String, String] = Map([])
for f in result.findings {
if !sources.contains(f.file) {
let content = @fs.read_file_to_string(f.file) catch { _ => "" }
sources[f.file] = content
}
}
let imports = @audit.ImportContext::empty()
let requests = @audit.generate_batch_prompts(
result.findings,
sources,
imports,
)
if format == "script" || format == "run" {
let script = @audit.generate_llm_script(llm_config, requests)
let script_file = match output_file {
Some(f) => f
None => "llm_analyze.py"
}
@fs.write_string_to_file(script_file, script) catch {
_ => {
println("Error writing script file")
return
}
}
println("Generated: " + script_file)
println("")
println("Run the analysis:")
println(" python3 " + script_file)
} else if format == "json" {
let buf = StringBuilder::new()
buf.write_string("[\n")
for i, req in requests {
if i > 0 {
buf.write_string(",\n")
}
buf.write_string(@audit.format_prompt_as_json(req))
}
buf.write_string("\n]")
write_output(buf.to_string(), output_file)
} else {
let buf = StringBuilder::new()
buf.write_string("# LLM Analysis Prompts\n\n")
buf.write_string(
"Use these prompts with Claude or other LLMs to verify findings.\n\n",
)
for i, req in requests {
buf.write_string("## Finding ")
buf.write_string((i + 1).to_string())
buf.write_string(": ")
buf.write_string(@audit.rule_id_str(req.finding.rule_id))
buf.write_string(" (")
buf.write_string(req.finding.file)
buf.write_string(":")
buf.write_string(req.finding.line.to_string())
buf.write_string(")\n\n")
buf.write_string("### System Prompt\n\n```\n")
buf.write_string(req.system_prompt)
buf.write_string("\n```\n\n")
buf.write_string("### User Prompt\n\n```\n")
buf.write_string(req.prompt)
buf.write_string("\n```\n\n---\n\n")
}
write_output(buf.to_string(), output_file)
}
}
///|
fn cmd_generate_poc(matches : @argparse.Matches) -> Unit {
let output_file = get_opt(matches, "output")
let path = get_val(matches, "path", ".")
println("moon-audit PoC Generator\n")
println("Scanning: " + path + "\n")
let result = match do_scan(matches) {
Some(r) => r
None => return
}
if result.findings.length() == 0 {
println("No findings to generate PoC for.")
return
}
let imports = @audit.ImportContext::empty()
let pocs = @audit.generate_all_pocs(result.findings, imports)
if pocs.length() == 0 {
println("No PoC templates available for the detected finding types.")
return
}
println("Generated " + pocs.length().to_string() + " PoC template(s)\n")
let report = @audit.format_poc_report(pocs)
write_output(report, output_file)
}
///|
fn cmd_remediate(matches : @argparse.Matches) -> Unit {
let output_file = get_opt(matches, "output")
let path = get_val(matches, "path", ".")
println("moon-audit Remediation Guide Generator\n")
println("Scanning: " + path + "\n")
let result = match do_scan(matches) {
Some(r) => r
None => return
}
if result.findings.length() == 0 {
println("No findings to remediate.")
return
}
let remediations = @audit.get_all_remediations(result.findings)
println(
"Generated remediation guide for " +
remediations.length().to_string() +
" rule(s)\n",
)
let report = @audit.format_remediation_report(remediations)
write_output(report, output_file)
}
///|
fn cmd_summary(matches : @argparse.Matches) -> Unit {
let format = get_val(matches, "format", "text")
let output_file = get_opt(matches, "output")
let path = get_val(matches, "path", ".")
println("moon-audit Scan Summary\n")
println("Scanning: " + path + "\n")
let result = match do_scan(matches) {
Some(r) => r
None => return
}
let rules = @audit.list_all_rules()
let summary = @audit.build_summary(result, rules)
let score = @audit.risk_score(summary)
let level = @audit.risk_level(score)
let output = if format == "json" {
@audit.format_summary_json(summary)
} else {
let text = @audit.format_summary_text(summary)
let buf = StringBuilder::new()
buf.write_string(text)
buf.write_string("\n Risk Score: ")
buf.write_string(score.to_string())
buf.write_string(" / 100 (")
buf.write_string(level)
buf.write_string(")\n")
buf.to_string()
}
write_output(output, output_file)
}
///|
fn cmd_pipeline(matches : @argparse.Matches) -> Unit {
let path = get_val(matches, "path", ".")
let out_dir = get_val(matches, "output-dir", "moon-audit-report")
let fail_on_error = match matches.flags.get("fail-on-error") {
Some(v) => v
None => false
}
let config_path = get_val(matches, "config", ".moon-audit.json")
let resolved_config = if config_path == ".moon-audit.json" {
@path.Path(path).join(@path.Path(config_path)).0
} else {
config_path
}
println("=".repeat(60))
println(" moon-audit Analysis Pipeline")
println("=".repeat(60))
println("")
println(" Target: " + path)
println(" Output: " + out_dir + "/")
println("")
let path_ok = @fs.is_dir(path) catch { _ => false }
if !path_ok {
println("moon-audit: error: path not found or not a directory: " + path)
return
}
@fs.create_dir(out_dir) catch {
err => {
let _ = err
println(
"moon-audit: error: failed to create output directory: " + out_dir,
)
return
}
}
let config = @audit.load_config(resolved_config)
// Phase 1: Static scan
println("[1/3] Static scan...")
let result = @audit.scan_project(path, config)
println(
" " +
result.files_scanned.to_string() +
" files scanned, " +
result.findings.length().to_string() +
" finding(s)",
)
let scan_text = @audit.format_text(result, true)
let scan_path = @path.Path(out_dir).join(@path.Path("scan-results.txt")).0
write_pipeline_file(scan_path, scan_text)
let sarif = @audit.format_sarif(result)
let sarif_path = @path.Path(out_dir).join(@path.Path("scan-results.sarif")).0
write_pipeline_file(sarif_path, sarif)
let json_out = @audit.format_json(result)
let json_path = @path.Path(out_dir).join(@path.Path("scan-results.json")).0
write_pipeline_file(json_path, json_out)
if result.findings.length() == 0 {
println("")
println("No findings detected. Scan results written to " + out_dir + "/")
println("Pipeline complete (phases 2-3 skipped: no findings to analyze).")
return
}
// Phase 2: Taint analysis
println("[2/3] Taint analysis...")
let sources : Map[String, String] = Map([])
for f in result.findings {
if !sources.contains(f.file) {
let content = @fs.read_file_to_string(f.file) catch { _ => "" }
sources[f.file] = content
}
}
let mod_imports = @audit.analyze_mod_imports(path)
let all_flows : Array[@audit.TaintFlow] = []
for file, source in sources {
let (impls, _) = @parser.parse_string(source, name=file)
let state = @audit.run_taint_analysis(impls, file, source, mod_imports)
for flow in state.flows {
all_flows.push(flow)
}
}
let taint_report = @audit.format_taint_report(all_flows)
let taint_path = @path.Path(out_dir).join(@path.Path("taint-analysis.txt")).0
write_pipeline_file(taint_path, taint_report)
println(" " + all_flows.length().to_string() + " taint flow(s) traced")
// Phase 3: Summary
println("[3/3] Summary report...")
let rules = @audit.list_all_rules()
let summary = @audit.build_summary(result, rules)
let score = @audit.risk_score(summary)
let level = @audit.risk_level(score)
let summary_text = @audit.format_summary_text(summary)
let summary_path = @path.Path(out_dir).join(@path.Path("summary.txt")).0
write_pipeline_file(summary_path, summary_text)
let summary_json = @audit.format_summary_json(summary)
let summary_json_path = @path.Path(out_dir).join(@path.Path("summary.json")).0
write_pipeline_file(summary_json_path, summary_json)
// Final output
println("")
println("=".repeat(60))
println(" Pipeline Complete")
println("=".repeat(60))
println("")
println(" Findings: " + result.findings.length().to_string())
println(" Risk Score: " + score.to_string() + " / 100 (" + level + ")")
println("")
println(" Output directory: " + out_dir + "/")
println(" scan-results.txt Static scan findings (text)")
println(" scan-results.json Static scan findings (JSON)")
println(" scan-results.sarif SARIF 2.1.0 (GitHub Code Scanning)")
println(" taint-analysis.txt Data flow taint traces")
println(" summary.txt Risk summary (text)")
println(" summary.json Risk summary (JSON)")
println("")
println(" Optional next steps:")
println(" moon-audit llm-analyze " + path + " # LLM-assisted triage")
println(" moon-audit generate-poc " + path + " # PoC exploit templates")
println(" moon-audit remediate " + path + " # Fix guide")
if fail_on_error {
let has_errors = result.findings
.iter()
.any(fn(f) { f.severity == @audit.Error })
if has_errors {
@sys.exit(1)
}
}
}
///|
fn cmd_generate_baseline(matches : @argparse.Matches) -> Unit {
let path = get_val(matches, "path", ".")
let config_path = get_val(matches, "config", ".moon-audit.json")
let output_path = get_val(matches, "output", ".moon-audit-baseline.json")
let resolved_config = if config_path == ".moon-audit.json" {
@path.Path(path).join(@path.Path(config_path)).0
} else {
config_path
}
let config = @audit.load_config(resolved_config)
let path_ok = @fs.is_dir(path) catch { _ => false }
if !path_ok {
println("moon-audit: error: path not found or not a directory: " + path)
return
}
println("moon-audit: generating baseline from: " + path)
let result = @audit.scan_project(path, config)
let baseline = @audit.generate_baseline(result.findings)
@fs.write_string_to_file(output_path, baseline) catch {
err => {
let _ = err
println("moon-audit: error: failed to write baseline to " + output_path)
return
}
}
println(
"Baseline written to: " +
output_path +
" (" +
result.findings.length().to_string() +
" findings)",
)
println("Use --baseline " + output_path + " to filter these in future scans")
}
///|
fn cmd_deep_audit(matches : @argparse.Matches) -> Unit {
let path = get_val(matches, "path", ".")
let output_path = get_val(matches, "output", "deep_audit.py")
let config_path = get_val(matches, "config", ".moon-audit.json")
let resolved_config = if config_path == ".moon-audit.json" {
@path.Path(path).join(@path.Path(config_path)).0
} else {
config_path
}
let config = @audit.load_config(resolved_config)
let path_ok = @fs.is_dir(path) catch { _ => false }
if !path_ok {
println("moon-audit: error: path not found: " + path)
return
}
println("moon-audit Deep Audit\n")
println("Scanning: " + path)
let result = @audit.scan_project(path, config)
println("Static findings: " + result.findings.length().to_string())
let source_files = @audit.collect_audit_files(path, config)
println("Source files collected: " + source_files.length().to_string())
let llm_config = @audit.load_llm_config(path)
println("LLM: " + llm_config.model + " @ " + llm_config.base_url)
if llm_config.api_key.length() == 0 {
println("\nWarning: No API key configured. Set LLM_API_KEY in .env")
}
println("")
let script = @audit.generate_deep_audit_script(
llm_config,
result.findings,
source_files,
)
@fs.write_string_to_file(output_path, script) catch {
_ => {
println("Error writing script: " + output_path)
return
}
}
println("Generated: " + output_path)
println("")
println("Run the deep audit:")
println(" python3 " + output_path)
println("")
println("The script will:")
println(
" 1. Verify " +
result.findings.length().to_string() +
" static findings with LLM (true/false positive)",
)
println(
" 2. Send " +
source_files.length().to_string() +
" source files for deep security review",
)
println(" 3. Save combined report to deep_audit_report.json")
}