// LLM-assisted vulnerability analysis prompt generation.
// Generates structured prompts from static analysis findings
// for LLM-based deep analysis and false positive reduction.
///|
pub(all) struct LLMAnalysisRequest {
finding : Finding
source_context : String
import_context : ImportContext
prompt : String
system_prompt : String
}
///|
pub(all) struct LLMAnalysisResult {
finding : Finding
is_true_positive : Bool
confidence : Double
explanation : String
severity_adjustment : Severity?
remediation : String
}
///|
pub fn generate_analysis_prompt(
finding : Finding,
source : String,
imports : ImportContext,
) -> LLMAnalysisRequest {
let context = extract_source_context(source, finding.line, 10)
let system = build_system_prompt()
let prompt = build_analysis_prompt(finding, context, imports)
{
finding,
source_context: context,
import_context: imports,
prompt,
system_prompt: system,
}
}
///|
pub fn generate_batch_prompts(
findings : Array[Finding],
sources : Map[String, String],
imports : ImportContext,
) -> Array[LLMAnalysisRequest] {
let requests : Array[LLMAnalysisRequest] = []
for f in findings {
let source = match sources.get(f.file) {
Some(s) => s
None => ""
}
requests.push(generate_analysis_prompt(f, source, imports))
}
requests
}
///|
fn build_system_prompt() -> String {
let buf = StringBuilder::new()
buf.write_string(
"You are a security expert specialized in MoonBit web application security.\n",
)
buf.write_string(
"Your task is to analyze potential vulnerabilities found by static analysis.\n\n",
)
buf.write_string("For each finding, determine:\n")
buf.write_string(
"1. Whether it is a true positive (real vulnerability) or false positive\n",
)
buf.write_string("2. Your confidence level (0.0 to 1.0)\n")
buf.write_string("3. Detailed explanation of your reasoning\n")
buf.write_string("4. Whether the severity should be adjusted\n")
buf.write_string("5. Specific remediation advice\n\n")
buf.write_string("Consider the following MoonBit-specific characteristics:\n")
buf.write_string(
"- MoonBit is statically typed with compile-time safety guarantees\n",
)
buf.write_string(
"- String::replace() only replaces the FIRST occurrence (unlike most languages)\n",
)
buf.write_string("- MoonBit compiles to Wasm, JS, C, or native targets\n")
buf.write_string(
"- Web frameworks (mocket, crescent) use route parameters via :param syntax\n",
)
buf.write_string(
"- The cmark library defaults to safe=false for HTML rendering\n\n",
)
buf.write_string(
"Respond in JSON format with fields: is_true_positive, confidence, explanation, severity_adjustment, remediation\n",
)
buf.to_string()
}
///|
fn build_analysis_prompt(
finding : Finding,
context : String,
imports : ImportContext,
) -> String {
let buf = StringBuilder::new()
buf.write_string("## Static Analysis Finding\n\n")
buf.write_string("**Rule**: ")
buf.write_string(rule_id_str(finding.rule_id))
buf.write_char('\n')
buf.write_string("**Severity**: ")
buf.write_string(severity_str(finding.severity))
buf.write_char('\n')
buf.write_string("**File**: ")
buf.write_string(finding.file)
buf.write_char(':')
buf.write_string(finding.line.to_string())
buf.write_char('\n')
buf.write_string("**Message**: ")
buf.write_string(finding.message)
buf.write_string("\n\n")
buf.write_string("### Source Context\n\n```moonbit\n")
buf.write_string(context)
buf.write_string("\n```\n\n")
buf.write_string("### Import Context\n\n")
write_import_context(buf, imports)
buf.write_string("\n### Question\n\n")
buf.write_string(
"Is this finding a true positive? Analyze the code context, ",
)
buf.write_string(
"data flow, and whether user-controlled input can reach this sink. ",
)
buf.write_string(
"Consider framework-specific behavior and MoonBit language semantics.\n",
)
buf.to_string()
}
///|
fn write_import_context(buf : StringBuilder, imports : ImportContext) -> Unit {
buf.write_string("- cmark: ")
buf.write_string(imports.has_cmark.to_string())
buf.write_char('\n')
buf.write_string("- rabbita: ")
buf.write_string(imports.has_rabbita.to_string())
buf.write_char('\n')
buf.write_string("- mocket: ")
buf.write_string(imports.has_mocket.to_string())
buf.write_char('\n')
buf.write_string("- crescent: ")
buf.write_string(imports.has_crescent.to_string())
buf.write_char('\n')
}
///|
fn extract_source_context(
source : String,
center_line : Int,
radius : Int,
) -> String {
let lines : Array[String] = []
for part in source.split("\n") {
lines.push(part.to_owned())
}
let start = if center_line - radius - 1 > 0 {
center_line - radius - 1
} else {
0
}
let end = if center_line + radius < lines.length() {
center_line + radius
} else {
lines.length()
}
let buf = StringBuilder::new()
for i = start; i < end; i = i + 1 {
let line_num = i + 1
let marker = if line_num == center_line { ">>> " } else { " " }
buf.write_string(marker)
buf.write_string(line_num.to_string())
buf.write_string(" | ")
buf.write_string(lines[i])
buf.write_char('\n')
}
buf.to_string()
}
///|
pub fn format_prompt_as_json(
req : LLMAnalysisRequest,
model? : String,
) -> String {
let model_name = model.unwrap_or("claude-sonnet-4-20250514")
let obj : Json = {
"model": Json::string(model_name),
"system": Json::string(req.system_prompt),
"messages": Json::array([
{ "role": Json::string("user"), "content": Json::string(req.prompt) },
]),
}
obj.stringify(indent=2)
}
///|
pub fn parse_llm_response(
finding : Finding,
response_json : String,
) -> LLMAnalysisResult? {
let json = @json.parse(response_json) catch { _ => return None }
match json {
Object(obj) => {
let is_tp = match obj.get("is_true_positive") {
Some(True) => true
_ => false
}
let confidence = match obj.get("confidence") {
Some(Number(n, ..)) => n
_ => 0.5
}
let explanation = match obj.get("explanation") {
Some(String(s)) => s
_ => ""
}
let sev_adj : Severity? = match obj.get("severity_adjustment") {
Some(String(s)) => Some(severity_from_str(s))
_ => None
}
let remediation = match obj.get("remediation") {
Some(String(s)) => s
_ => ""
}
Some({
finding,
is_true_positive: is_tp,
confidence,
explanation,
severity_adjustment: sev_adj,
remediation,
})
}
_ => None
}
}
///|
pub fn filter_by_llm_results(
results : Array[LLMAnalysisResult],
min_confidence : Double,
) -> Array[LLMAnalysisResult] {
results.filter(fn(r) { r.is_true_positive && r.confidence >= min_confidence })
}
///|
pub fn format_llm_report(results : Array[LLMAnalysisResult]) -> String {
let buf = StringBuilder::new()
buf.write_string("# LLM-Assisted Vulnerability Analysis Report\n\n")
let tp_count = results.iter().filter(fn(r) { r.is_true_positive }).count()
let fp_count = results.length() - tp_count
buf.write_string("**Total findings analyzed**: ")
buf.write_string(results.length().to_string())
buf.write_char('\n')
buf.write_string("**True positives**: ")
buf.write_string(tp_count.to_string())
buf.write_char('\n')
buf.write_string("**False positives**: ")
buf.write_string(fp_count.to_string())
buf.write_string("\n\n")
for i, r in results {
buf.write_string("## Finding ")
buf.write_string((i + 1).to_string())
buf.write_string(": ")
buf.write_string(rule_id_str(r.finding.rule_id))
buf.write_char('\n')
buf.write_string("- **File**: ")
buf.write_string(r.finding.file)
buf.write_char(':')
buf.write_string(r.finding.line.to_string())
buf.write_char('\n')
buf.write_string("- **Verdict**: ")
buf.write_string(
if r.is_true_positive {
"TRUE POSITIVE"
} else {
"FALSE POSITIVE"
},
)
buf.write_char('\n')
buf.write_string("- **Confidence**: ")
buf.write_string(r.confidence.to_string())
buf.write_char('\n')
buf.write_string("- **Explanation**: ")
buf.write_string(r.explanation)
buf.write_char('\n')
if r.remediation.length() > 0 {
buf.write_string("- **Remediation**: ")
buf.write_string(r.remediation)
buf.write_char('\n')
}
buf.write_char('\n')
}
buf.to_string()
}