diff --git a/.gitignore b/.gitignore index 4e70370..687c911 100644 --- a/.gitignore +++ b/.gitignore @@ -106,3 +106,6 @@ credentials.json # LangGraph API server .langgraph_api/ + +# Reports +report* \ No newline at end of file diff --git a/README.md b/README.md index ab84623..aeaa7d1 100644 --- a/README.md +++ b/README.md @@ -109,6 +109,13 @@ export SKILLSPECTOR_PROVIDER=nv_build export NVIDIA_INFERENCE_KEY=nvapi-... skillspector scan ./my-skill/ +# Gemini (via OpenAI compatibility layer) +export SKILLSPECTOR_PROVIDER=openai +export OPENAI_API_KEY="YOUR_GEMINI_API_KEY" +export OPENAI_BASE_URL="https://generativelanguage.googleapis.com/v1beta/openai/" +export SKILLSPECTOR_MODEL=gemini-3.5-flash +skillspector scan ./my-skill/ + # Local Ollama or any OpenAI-compatible endpoint export SKILLSPECTOR_PROVIDER=openai export OPENAI_API_KEY=ollama diff --git a/model_registry.yaml b/model_registry.yaml index e1c2b8c..7a1e34f 100644 --- a/model_registry.yaml +++ b/model_registry.yaml @@ -40,3 +40,7 @@ models: "openai/openai/gpt-5.3-chat": context_length: 128000 max_output_tokens: 16384 + + "gemini-3.5-flash": + context_length: 1048576 + max_output_tokens: 8192 diff --git a/src/skillspector/cli.py b/src/skillspector/cli.py index 83e3224..7e1993b 100644 --- a/src/skillspector/cli.py +++ b/src/skillspector/cli.py @@ -232,7 +232,89 @@ def scan( "version": __version__, }, } - result = graph.invoke(state, config=trace_config) + if verbose: + result = graph.invoke(state, config=trace_config) + else: + from rich.progress import Progress, SpinnerColumn, TextColumn, BarColumn, TimeElapsedColumn + from rich.console import Console + from skillspector.nodes.analyzers import ANALYZER_NODE_IDS + import warnings + + # Suppress noisy Pydantic serialization warnings during structured LLM output + warnings.filterwarnings("ignore", category=UserWarning, module="pydantic") + + total_steps = 4 + len(ANALYZER_NODE_IDS) + result = dict(state) + + # Use stderr for progress so stdout remains clean for structured outputs + err_console = Console(stderr=True) + + with Progress( + SpinnerColumn(), + TextColumn("[progress.description]{task.description}"), + BarColumn(), + TextColumn("[progress.percentage]{task.percentage:>3.0f}%"), + TimeElapsedColumn(), + console=err_console, + transient=True, + ) as progress: + task_id = progress.add_task("Resolving input...", total=total_steps) + + num_files = 0 + analyzers_done = 0 + total_analyzers = len(ANALYZER_NODE_IDS) + + for update in graph.stream(state, config=trace_config, stream_mode="updates"): + for node_name, node_output in update.items(): + progress.advance(task_id) + + # Accumulate scalar outputs needed by the CLI (report_body, risk_score, temp_dir, sarif_report) + if "temp_dir_for_cleanup" in node_output: + result["temp_dir_for_cleanup"] = node_output["temp_dir_for_cleanup"] + if "report_body" in node_output: + result["report_body"] = node_output["report_body"] + if "sarif_report" in node_output: + result["sarif_report"] = node_output["sarif_report"] + if "risk_score" in node_output: + result["risk_score"] = node_output["risk_score"] + + # Update UI text based on graph progression + if node_name == "resolve_input": + progress.update(task_id, description="Building context...") + elif node_name == "build_context": + components = node_output.get("components", []) + num_files = len(components) + progress.update(task_id, description=f"Analyzing {num_files} files (0/{total_analyzers} rules applied)...") + + # Print a proper report of the files and directories being scanned + from rich.tree import Tree + from pathlib import Path + + tree = Tree("[bold blue]Discovered Files to Scan[/bold blue]") + nodes = {"": tree} + for path in sorted(components): + parts = Path(path).parts + current = "" + for part in parts: + parent = current + current = f"{current}/{part}" if current else part + if current not in nodes: + is_file = current == path + icon = "📄 " if is_file else "📁 " + style = "green" if is_file else "cyan" + nodes[current] = nodes[parent].add(f"[{style}]{icon}{part}[/{style}]") + + err_console.print(tree) + err_console.print() + + elif node_name in ANALYZER_NODE_IDS: + analyzers_done += 1 + progress.update(task_id, description=f"Analyzing {num_files} files ({analyzers_done}/{total_analyzers} rules applied)...") + # Print which rule just finished above the progress bar + err_console.print(f"[dim]✔ Rule completed: {node_name}[/dim]") + elif node_name == "meta_analyzer": + progress.update(task_id, description="Generating report...") + err_console.print("[dim]✔ Rule completed: meta_analyzer (filtering findings)[/dim]") _write_result(result, output, format) diff --git a/src/skillspector/nodes/analyzers/mcp_tool_poisoning.py b/src/skillspector/nodes/analyzers/mcp_tool_poisoning.py index d32bc6e..40e07cc 100644 --- a/src/skillspector/nodes/analyzers/mcp_tool_poisoning.py +++ b/src/skillspector/nodes/analyzers/mcp_tool_poisoning.py @@ -799,8 +799,8 @@ def _check_tp4(state: SkillspectorState) -> list[Finding]: ) ] - except Exception: - logger.warning("%s: TP4 LLM check failed, skipping", ANALYZER_ID, exc_info=True) + except Exception as exc: + logger.warning("%s: TP4 LLM check failed, skipping: %s", ANALYZER_ID, exc) return [] diff --git a/src/skillspector/providers/nv_build/model_registry.yaml b/src/skillspector/providers/nv_build/model_registry.yaml index aeba04e..bdea12e 100644 --- a/src/skillspector/providers/nv_build/model_registry.yaml +++ b/src/skillspector/providers/nv_build/model_registry.yaml @@ -26,3 +26,7 @@ models: "openai/gpt-oss-120b": context_length: 128000 max_output_tokens: 16384 + + "gemini-3.5-flash": + context_length: 1048576 + max_output_tokens: 8192 diff --git a/src/skillspector/providers/openai/model_registry.yaml b/src/skillspector/providers/openai/model_registry.yaml index a4d2606..a539ccc 100644 --- a/src/skillspector/providers/openai/model_registry.yaml +++ b/src/skillspector/providers/openai/model_registry.yaml @@ -12,3 +12,7 @@ models: "gpt-5.4": context_length: 1000000 max_output_tokens: 128000 + + "gemini-3.5-flash": + context_length: 1048576 + max_output_tokens: 8192