How the SkillSpector Two-Stage Detection Pipeline Works: Static Analysis + LLM Meta-Analysis
SkillSpector's two-stage detection pipeline combines high-recall static analysis with LLM-driven meta-analysis to filter false positives and enrich confirmed vulnerabilities with context-aware remediation steps.
NVIDIA's SkillSpector is an open-source security scanner for AI skill bundles that implements a novel two-stage detection pipeline to balance performance with accuracy. The architecture separates coarse-grained detection from precision filtering, using LangGraph to orchestrate the workflow across distinct static and AI-powered phases. This design minimizes expensive LLM calls while maintaining deep contextual analysis of potential security issues.
Stage 1: Static Analysis for High-Recall Detection
The first phase employs a collection of per-file analyzers that scan source code, configuration files, and other artifacts within the skill bundle. These analyzers operate in parallel across the entire codebase to maximize coverage and recall.
In src/skillspector/nodes/analyzers/__init__.py, the system defines ANALYZER_NODE_IDS and ANALYZER_NODES, which map to specific detection implementations:
- YARA rule matching (
static_yara.py) – Pattern-based detection for known malicious signatures - Semantic intent analysis (
semantic_developer_intent.py) – Behavioral AST analysis to identify suspicious coding patterns - Taint-tracking engines – Data-flow analysis to trace untrusted inputs through the codebase
Each analyzer emits raw findings containing rule IDs, file locations, severity scores, and metadata. These findings represent a superset of potential issues—designed for high recall rather than precision. The results are stored as Finding objects in the shared state defined in src/skillspector/state.py.
Stage 2: LLM Meta-Analysis for Precision Filtering
The second phase introduces the Meta-Analyzer (src/skillspector/nodes/meta_analyzer.py), which transforms raw static findings into validated, actionable security intelligence. Rather than analyzing individual findings in isolation, the meta-analyzer performs per-file LLM calls that consider the complete context of the file alongside all detected issues.
For each file that generated at least one finding, the Meta-Analyzer:
- Constructs a structured prompt containing file metadata, source content, and the complete list of raw findings
- Invokes the configured LLM (e.g., Gemini 1.5 Pro) through the
LLMMetaAnalyzerclass - Receives a structured
MetaAnalyzerResultcontaining filtered true positives
This stage enriches confirmed vulnerabilities with confidence scores, intent classification, impact assessment, and human-readable remediation guidance. The base LLM functionality, including batch handling and token-budget logic, is implemented in src/skillspector/llm_analyzer_base.py.
LangGraph Workflow Architecture
The two-stage detection pipeline is implemented as a directed graph in src/skillspector/graph.py using LangGraph's StateGraph. The workflow connects five primary node types:
from langgraph.graph import StateGraph, START, END
from skillspector.state import SkillspectorState
workflow = StateGraph(SkillspectorState)
# Core pipeline nodes
workflow.add_node("resolve_input", resolve_input)
workflow.add_node("build_context", build_context)
workflow.add_node("meta_analyzer", meta_analyzer)
workflow.add_node("report", report)
# Dynamic analyzer nodes
for analyzer_id in ANALYZER_NODE_IDS:
workflow.add_node(analyzer_id, ANALYZER_NODES[analyzer_id])
# Execution flow
workflow.add_edge(START, "resolve_input")
workflow.add_edge("resolve_input", "build_context")
for analyzer_id in ANALYZER_NODE_IDS:
workflow.add_edge("build_context", analyzer_id)
workflow.add_edge(analyzer_id, "meta_analyzer")
workflow.add_edge("meta_analyzer", "report")
workflow.add_edge("report", END)
The execution sequence follows this path:
- Step 0:
resolve_inputparses command-line arguments or ZIP bundles, storing manifests and file caches - Step 1:
build_contextloads every file into the state for analyzer access - Step 2: All static analyzers run in parallel against the file context
- Step 3:
meta_analyzerprocesses files with findings through the LLM validation layer - Step 4:
reportgenerates SARIF/JSON outputs or console summaries
Implementation Details and Key Files
The state management system in src/skillspector/state.py maintains the SkillspectorState object, which tracks findings, file caches, and the bundle manifest throughout the pipeline.
You can invoke the two-stage detection pipeline programmatically:
from skillspector.graph import graph
from skillspector.state import SkillspectorState
state = SkillspectorState()
state["bundle_path"] = "./my_skill.zip"
state["use_llm"] = True
state["model_config"] = {"meta_analyzer": "gemini-1.5-pro"}
# Execute the compiled graph
final_state = graph.invoke(state)
print(final_state["report"])
Or via the CLI:
skillspector scan ./my_skill.zip --model meta_analyzer=gemini-1.5-pro
The architecture deliberately batches LLM calls at the file level rather than the finding level, significantly reducing API costs while maintaining the reasoning capabilities necessary for accurate vulnerability classification.
Summary
- Stage 1 uses multiple static analyzers (YARA, AST-based, taint-tracking) to generate high-recall raw findings across all files in
src/skillspector/nodes/analyzers/ - Stage 2 leverages the
meta_analyzernode to perform per-file LLM validation, filtering false positives and enriching true vulnerabilities with remediation steps - The LangGraph architecture in
src/skillspector/graph.pyorchestrates the pipeline throughresolve_input→build_context→ static analyzers →meta_analyzer→report - Cost optimization is achieved by limiting LLM calls to one per file with findings, rather than per individual detection
Frequently Asked Questions
How does SkillSpector minimize LLM API costs while maintaining accuracy?
SkillSpector batches LLM invocations at the file level rather than analyzing each finding separately. In src/skillspector/nodes/meta_analyzer.py, the Meta-Analyzer aggregates all raw findings from a single file into one prompt, generating a single MetaAnalyzerResult. This approach typically reduces LLM calls from dozens (per-finding) to one (per-file), while the LLM's reasoning capabilities still filter false positives and enrich confirmed issues with context.
What types of static analyzers run in the first stage?
The static analysis stage runs a diverse suite of detectors defined in src/skillspector/nodes/analyzers/, including YARA pattern matching (static_yara.py), semantic developer intent analysis (semantic_developer_intent.py), and taint-tracking engines. These analyzers are registered in the ANALYZER_NODES dictionary in __init__.py and execute in parallel during the LangGraph workflow.
Can the two-stage detection pipeline run without LLM meta-analysis?
Yes. The use_llm flag in the SkillspectorState allows execution to skip the meta-analysis stage. In this mode, the system produces raw static findings without LLM validation, though this results in higher false-positive rates. The pipeline remains functional as the report node processes findings from the state regardless of whether they underwent meta-analysis enrichment.
What output format does the meta-analyzer produce?
The Meta-Analyzer generates structured MetaAnalyzerResult objects containing validated vulnerabilities with confidence scores, intent classifications (e.g., malicious, suspicious, benign), impact assessments, and human-readable remediation steps. These enriched findings are then converted to SARIF or JSON format in src/skillspector/nodes/report.py for integration with security dashboards and CI/CD pipelines.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →