// .env file parser for LLM API configuration.
///|
pub(all) struct LLMConfig {
base_url : String
model : String
api_key : String
}
///|
pub fn LLMConfig::default() -> LLMConfig {
{
base_url: "https://api.anthropic.com/v1/messages",
model: "claude-sonnet-4-20250514",
api_key: "",
}
}
///|
pub fn load_env(dir : String) -> Map[String, String] {
let env : Map[String, String] = Map([])
let env_path = @path.Path(dir).join(@path.Path(".env")).0
let content = @fs.read_file_to_string(env_path) catch { _ => return env }
parse_env_content(content, env)
env
}
///|
fn parse_env_content(content : String, env : Map[String, String]) -> Unit {
let lines : Array[String] = []
for part in content.split("\n") {
lines.push(part.to_owned())
}
for line in lines {
let trimmed = line.trim().to_owned()
if trimmed.length() == 0 || trimmed.has_prefix("#") {
continue
}
let eq_pos = find_char_in_str(trimmed, '=')
if eq_pos < 0 {
continue
}
let key = trimmed[0:eq_pos].to_owned().trim().to_owned()
let raw_val = trimmed[eq_pos + 1:].to_owned().trim().to_owned()
let val = strip_quotes(raw_val)
env[key] = val
}
}
///|
fn char16(c : Char) -> UInt16 {
c.to_uint().to_uint16()
}
///|
fn find_char_in_str(s : String, c : Char) -> Int {
let target = char16(c)
for i = 0; i < s.length(); i = i + 1 {
if s[i] == target {
return i
}
}
-1
}
///|
fn strip_quotes(s : String) -> String {
if s.length() >= 2 {
let dq = char16('"')
let sq = char16('\'')
let first = s[0]
let last = s[s.length() - 1]
if (first == dq && last == dq) || (first == sq && last == sq) {
return s[1:s.length() - 1].to_owned()
}
}
s
}
///|
pub fn load_llm_config(dir : String) -> LLMConfig {
let env = load_env(".")
if env.length() == 0 {
let env2 = load_env(dir)
return build_llm_config(env2)
}
build_llm_config(env)
}
///|
fn env_get_any(env : Map[String, String], keys : Array[String]) -> String {
for key in keys {
match env.get(key) {
Some(v) => if v.length() > 0 { return v }
None => ()
}
}
""
}
///|
fn build_llm_config(env : Map[String, String]) -> LLMConfig {
let base = LLMConfig::default()
let url = env_get_any(env, ["LLM_BASE_URL", "Base_URL", "BASE_URL"])
let base_url = if url.length() > 0 { url } else { base.base_url }
let m = env_get_any(env, ["LLM_MODEL", "Model", "MODEL"])
let model = if m.length() > 0 { m } else { base.model }
let k = env_get_any(env, ["LLM_API_KEY", "API_KEY", "ANTHROPIC_API_KEY"])
let api_key = if k.length() > 0 { k } else { base.api_key }
{ base_url, model, api_key }
}
///|
pub fn load_env_merged(dir : String) -> Map[String, String] {
let env = load_env(".")
let env2 = load_env(dir)
for k, v in env2 {
if !env.contains(k) {
env[k] = v
}
}
env
}
///|
pub fn generate_llm_script(
llm_config : LLMConfig,
requests : Array[LLMAnalysisRequest],
) -> String {
let buf = StringBuilder::new()
buf.write_string("#!/usr/bin/env python3\n")
buf.write_string("\"\"\"moon-audit LLM Analysis Runner\n")
buf.write_string(
"Auto-generated script. Calls LLM API to verify static analysis findings.\n",
)
buf.write_string("Usage: python3 llm_analyze.py\n")
buf.write_string("\"\"\"\n\n")
buf.write_string("import json, os, sys, time\n")
buf.write_string("from pathlib import Path\n\n")
buf.write_string("try:\n")
buf.write_string(" import httpx\n")
buf.write_string(" CLIENT = httpx.Client(timeout=60)\n")
buf.write_string(" def post(url, headers, body):\n")
buf.write_string(" r = CLIENT.post(url, headers=headers, json=body)\n")
buf.write_string(" return r.status_code, r.json()\n")
buf.write_string("except ImportError:\n")
buf.write_string(" from urllib.request import Request, urlopen\n")
buf.write_string(" def post(url, headers, body):\n")
buf.write_string(
" req = Request(url, data=json.dumps(body).encode(), headers=headers, method='POST')\n",
)
buf.write_string(" resp = urlopen(req)\n")
buf.write_string(" return resp.status, json.loads(resp.read())\n\n")
buf.write_string("# ── Load .env file ──\n")
buf.write_string("def load_dotenv():\n")
buf.write_string(" env = {}\n")
buf.write_string(
" for p in [Path('.env'), Path(__file__).parent / '.env']:\n",
)
buf.write_string(" if p.is_file():\n")
buf.write_string(" for line in p.read_text().splitlines():\n")
buf.write_string(" line = line.strip()\n")
buf.write_string(
" if not line or line.startswith('#') or '=' not in line:\n",
)
buf.write_string(" continue\n")
buf.write_string(" k, v = line.split('=', 1)\n")
buf.write_string(" v = v.strip().strip('\"').strip(\"'\")\n")
buf.write_string(" env[k.strip()] = v\n")
buf.write_string(" break\n")
buf.write_string(" return env\n\n")
buf.write_string("_dotenv = load_dotenv()\n")
buf.write_string("def _cfg(key, default=''):\n")
buf.write_string(
" return _dotenv.get(key) or os.environ.get(key) or default\n\n",
)
buf.write_string(
"# ── Configuration (.env → env var → default) ──\n",
)
buf.write_string("BASE_URL = _cfg('LLM_BASE_URL', '")
buf.write_string(escape_py_str(llm_config.base_url))
buf.write_string("')\n")
buf.write_string("MODEL = _cfg('LLM_MODEL', '")
buf.write_string(escape_py_str(llm_config.model))
buf.write_string("')\n")
buf.write_string(
"API_KEY = _cfg('LLM_API_KEY') or _cfg('API_KEY') or _cfg('ANTHROPIC_API_KEY')\n\n",
)
buf.write_string("if not API_KEY:\n")
buf.write_string(" print('ERROR: No API key configured.')\n")
buf.write_string(
" print('Set LLM_API_KEY or ANTHROPIC_API_KEY in .env file:')\n",
)
buf.write_string(" print(' LLM_API_KEY=sk-...')\n")
buf.write_string(
" print(' LLM_BASE_URL=https://api.anthropic.com/v1/messages # optional')\n",
)
buf.write_string(
" print(' LLM_MODEL=claude-sonnet-4-20250514 # optional')\n",
)
buf.write_string(" sys.exit(1)\n\n")
write_py_is_anthropic(buf)
write_py_call_function(buf)
write_py_findings(buf, requests)
write_py_main(buf)
buf.to_string()
}
///|
fn write_py_is_anthropic(buf : StringBuilder) -> Unit {
buf.write_string("IS_ANTHROPIC = \"anthropic\" in BASE_URL\n\n")
buf.write_string("# Auto-append endpoint path if BASE_URL is just the base\n")
buf.write_string(
"if not IS_ANTHROPIC and not BASE_URL.endswith(\"/chat/completions\"):\n",
)
buf.write_string(
" BASE_URL = BASE_URL.rstrip(\"/\") + \"/chat/completions\"\n\n",
)
}
///|
fn write_py_call_function(buf : StringBuilder) -> Unit {
buf.write_string("def call_llm(system_prompt, user_prompt):\n")
buf.write_string(" if IS_ANTHROPIC:\n")
buf.write_string(" headers = {\n")
buf.write_string(" \"x-api-key\": API_KEY,\n")
buf.write_string(" \"content-type\": \"application/json\",\n")
buf.write_string(" \"anthropic-version\": \"2023-06-01\",\n")
buf.write_string(" }\n")
buf.write_string(" body = {\n")
buf.write_string(" \"model\": MODEL,\n")
buf.write_string(" \"max_tokens\": 2048,\n")
buf.write_string(" \"system\": system_prompt,\n")
buf.write_string(
" \"messages\": [{\"role\": \"user\", \"content\": user_prompt}],\n",
)
buf.write_string(" }\n")
buf.write_string(" else:\n")
buf.write_string(" # OpenAI-compatible API\n")
buf.write_string(" headers = {\n")
buf.write_string(" \"Authorization\": f\"Bearer {API_KEY}\",\n")
buf.write_string(" \"Content-Type\": \"application/json\",\n")
buf.write_string(" }\n")
buf.write_string(" body = {\n")
buf.write_string(" \"model\": MODEL,\n")
buf.write_string(" \"max_tokens\": 2048,\n")
buf.write_string(" \"messages\": [\n")
buf.write_string(
" {\"role\": \"system\", \"content\": system_prompt},\n",
)
buf.write_string(
" {\"role\": \"user\", \"content\": user_prompt},\n",
)
buf.write_string(" ],\n")
buf.write_string(" }\n")
buf.write_string(" status, resp = post(BASE_URL, headers, body)\n")
buf.write_string(" if status != 200:\n")
buf.write_string(" print(f\" API error: {status} - {resp}\")\n")
buf.write_string(" return None\n")
buf.write_string(" if IS_ANTHROPIC:\n")
buf.write_string(" text = resp[\"content\"][0][\"text\"]\n")
buf.write_string(" else:\n")
buf.write_string(
" text = resp[\"choices\"][0][\"message\"][\"content\"]\n",
)
buf.write_string(" # Extract JSON from response\n")
buf.write_string(" try:\n")
buf.write_string(" start = text.index(\"{\")\n")
buf.write_string(" end = text.rindex(\"}\") + 1\n")
buf.write_string(" return json.loads(text[start:end])\n")
buf.write_string(" except (ValueError, json.JSONDecodeError):\n")
buf.write_string(
" print(f\" Failed to parse LLM response as JSON\")\n",
)
buf.write_string(" return None\n\n")
}
///|
fn write_py_findings(
buf : StringBuilder,
requests : Array[LLMAnalysisRequest],
) -> Unit {
buf.write_string("FINDINGS = [\n")
for req in requests {
buf.write_string(" {\n")
buf.write_string(" \"rule_id\": \"")
buf.write_string(escape_py_str(rule_id_str(req.finding.rule_id)))
buf.write_string("\",\n")
buf.write_string(" \"file\": \"")
buf.write_string(escape_py_str(req.finding.file))
buf.write_string("\",\n")
buf.write_string(" \"line\": ")
buf.write_string(req.finding.line.to_string())
buf.write_string(",\n")
buf.write_string(" \"message\": \"")
buf.write_string(escape_py_str(req.finding.message))
buf.write_string("\",\n")
buf.write_string(" \"severity\": \"")
buf.write_string(escape_py_str(severity_str(req.finding.severity)))
buf.write_string("\",\n")
buf.write_string(" \"system_prompt\": ")
write_py_triple_quoted(buf, req.system_prompt)
buf.write_string(",\n")
buf.write_string(" \"user_prompt\": ")
write_py_triple_quoted(buf, req.prompt)
buf.write_string(",\n")
buf.write_string(" },\n")
}
buf.write_string("]\n\n")
}
///|
fn write_py_main(buf : StringBuilder) -> Unit {
buf.write_string("def main():\n")
buf.write_string(" print(f\"moon-audit LLM Analysis\")\n")
buf.write_string(" print(f\"API: {BASE_URL}\")\n")
buf.write_string(" print(f\"Model: {MODEL}\")\n")
buf.write_string(" print(f\"Findings to analyze: {len(FINDINGS)}\")\n")
buf.write_string(" print(\"=\" * 60)\n")
buf.write_string(" print()\n\n")
buf.write_string(" results = []\n")
buf.write_string(" for i, f in enumerate(FINDINGS):\n")
buf.write_string(
" label = f\"{f['rule_id']} @ {f['file']}:{f['line']}\"\n",
)
buf.write_string(
" print(f\"[{i+1}/{len(FINDINGS)}] Analyzing: {label}\")\n",
)
buf.write_string(
" result = call_llm(f[\"system_prompt\"], f[\"user_prompt\"])\n",
)
buf.write_string(" if result:\n")
buf.write_string(" tp = result.get(\"is_true_positive\", False)\n")
buf.write_string(" conf = result.get(\"confidence\", 0)\n")
buf.write_string(
" verdict = \"TRUE POSITIVE\" if tp else \"FALSE POSITIVE\"\n",
)
buf.write_string(
" print(f\" => {verdict} (confidence: {conf})\")\n",
)
buf.write_string(
" print(f\" => {result.get('explanation', '')[:120]}\")\n",
)
buf.write_string(" results.append({**f, **result})\n")
buf.write_string(" else:\n")
buf.write_string(" print(f\" => SKIPPED (API error)\")\n")
buf.write_string(
" results.append({**f, \"is_true_positive\": None, \"confidence\": 0})\n",
)
buf.write_string(" if i < len(FINDINGS) - 1:\n")
buf.write_string(" time.sleep(1)\n")
buf.write_string(" print()\n\n")
buf.write_string(" # Summary\n")
buf.write_string(
" tp = [r for r in results if r.get(\"is_true_positive\") is True]\n",
)
buf.write_string(
" fp = [r for r in results if r.get(\"is_true_positive\") is False]\n",
)
buf.write_string(
" skipped = [r for r in results if r.get(\"is_true_positive\") is None]\n",
)
buf.write_string(" print(\"=\" * 60)\n")
buf.write_string(" print(f\"Analysis Complete\")\n")
buf.write_string(" print(f\" Total: {len(results)}\")\n")
buf.write_string(" print(f\" True Positives: {len(tp)}\")\n")
buf.write_string(" print(f\" False Positives: {len(fp)}\")\n")
buf.write_string(" print(f\" Skipped: {len(skipped)}\")\n")
buf.write_string(" print()\n\n")
buf.write_string(" if tp:\n")
buf.write_string(" print(\"Confirmed Vulnerabilities:\")\n")
buf.write_string(" for r in tp:\n")
buf.write_string(
" print(f\" [{r['severity'].upper()}] {r['rule_id']} @ {r['file']}:{r['line']}\")\n",
)
buf.write_string(
" print(f\" {r.get('explanation', '')[:200]}\")\n",
)
buf.write_string(" if r.get('remediation'):\n")
buf.write_string(
" print(f\" Fix: {r['remediation'][:200]}\")\n",
)
buf.write_string(" print()\n\n")
buf.write_string(" # Save JSON results\n")
buf.write_string(" out_file = \"llm_analysis_results.json\"\n")
buf.write_string(" with open(out_file, \"w\") as f:\n")
buf.write_string(
" json.dump(results, f, indent=2, ensure_ascii=False)\n",
)
buf.write_string(" print(f\"Full results saved to: {out_file}\")\n\n")
buf.write_string("if __name__ == \"__main__\":\n")
buf.write_string(" main()\n")
}
///|
fn escape_py_str(s : String) -> String {
let buf = StringBuilder::new()
let backslash = char16('\\')
let dquote = char16('"')
let newline = char16('\n')
let cr = char16('\r')
for i = 0; i < s.length(); i = i + 1 {
let c = s[i]
if c == backslash {
buf.write_string("\\\\")
} else if c == dquote {
buf.write_string("\\\"")
} else if c == newline {
buf.write_string("\\n")
} else if c == cr {
buf.write_string("\\r")
} else {
buf.write_string(c.to_int().unsafe_to_char().to_string())
}
}
buf.to_string()
}
///|
fn write_py_triple_quoted(buf : StringBuilder, s : String) -> Unit {
let backslash = char16('\\')
let dquote = char16('"')
buf.write_string("\"\"\"")
for i = 0; i < s.length(); i = i + 1 {
let c = s[i]
if c == backslash {
buf.write_string("\\\\")
} else if c == dquote {
let is_triple = i + 2 < s.length() &&
s[i + 1] == dquote &&
s[i + 2] == dquote
if is_triple {
buf.write_string("\\\"")
} else {
buf.write_char('"')
}
} else {
buf.write_string(c.to_int().unsafe_to_char().to_string())
}
}
buf.write_string("\"\"\"")
}