Skip to content

Commit 245fe46

Browse files
committed
feat(cli): Add rich terminal output formatting
- Switched to use instead of JSON in TTY environments. - Introduced rules to break up pipeline execution phases (Plan, Iteration X, Intelligence Processing). - Added to vividly display AI deductive reasoning during cluster merges.
1 parent e8e72ee commit 245fe46

6 files changed

Lines changed: 1563 additions & 1523 deletions

File tree

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ venv/
33
env/
44
.venv/
55
.env
6+
.sisyphus
67

78
# Application results
89
results/
@@ -24,3 +25,5 @@ htmlcov/
2425
.idea/
2526
.vscode/
2627
.cursor/
28+
test_output.txt
29+
all_tests.txt

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ dependencies = [
1919
"redis==5.0.8",
2020
"networkx==3.3",
2121
"asyncpg==0.30.0",
22+
"rich>=14.3.3",
2223
]
2324

2425
[project.optional-dependencies]

src/secnodeapi/ai/validate.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,11 @@
1212

1313
from ..vulnerability_models import Finding, TestResult
1414
from .llm_client import call_llm
15+
from rich.console import Console
16+
from rich.panel import Panel
1517

1618
logger = structlog.get_logger(__name__)
19+
console = Console()
1720

1821

1922
class AIValidationPayload(BaseModel):
@@ -263,6 +266,13 @@ async def deduplicate_findings_with_ai(findings: List[Finding]) -> List[Finding]
263266

264267
if len(cluster_findings) > 1:
265268
logger.info("AI Merge Decision", reasoning=cluster.reasoning, merged_count=len(cluster_findings), kept_id=cluster_findings[0].test_case_id)
269+
console.print(
270+
Panel(
271+
f"[bold cyan]Rationale:[/]\n{cluster.reasoning}\n\n[bold green]Action:[/]\nMerged [bold]{len(cluster_findings)}[/bold] duplicate findings into root cause [bold]{cluster_findings[0].test_case_id}[/bold].",
272+
title="🧠 AI Vulnerability Deduplication",
273+
border_style="cyan"
274+
)
275+
)
266276

267277
# Keep the finding with the highest CVSS or confidence
268278
top_finding = sorted(

src/secnodeapi/cli.py

Lines changed: 31 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,26 @@
1818
except ImportError:
1919
pass
2020

21-
structlog.configure(
22-
processors=[
23-
structlog.processors.TimeStamper(fmt="iso"),
24-
structlog.processors.JSONRenderer(),
25-
]
26-
)
21+
import sys
22+
from rich.console import Console
23+
24+
console = Console()
25+
26+
if sys.stdout.isatty() and not os.environ.get("SECNODE_JSON_LOGS"):
27+
structlog.configure(
28+
processors=[
29+
structlog.processors.TimeStamper(fmt="iso"),
30+
structlog.dev.ConsoleRenderer(colors=True),
31+
]
32+
)
33+
else:
34+
structlog.configure(
35+
processors=[
36+
structlog.processors.TimeStamper(fmt="iso"),
37+
structlog.processors.JSONRenderer(),
38+
]
39+
)
40+
2741
logger = structlog.get_logger(__name__)
2842

2943
from .config import RuntimeConfig, has_supported_provider_key
@@ -293,21 +307,24 @@ async def _run_schema_only(args, runtime: RuntimeConfig) -> None:
293307

294308

295309
async def _run_dry_run(args, pipeline_input: PipelineInput) -> None:
310+
console.rule("[bold cyan]SecNode Dry-Run Mode")
296311
_, tests = await build_pipeline_artifacts(pipeline_input)
297312
if not tests:
298313
logger.error("No tests generated. Aborting.")
299314
raise SystemExit(1)
300-
print(f"Generated {len(tests)} test cases (dry-run).")
315+
console.print(f"[bold green]Generated {len(tests)} test cases (dry-run).[/]")
301316
_write_dry_run_output(tests, args.dry_run_output)
302317
logger.info("Dry-run mode complete", generated_tests=len(tests))
303318

304319

305320
async def _run_full_pipeline(args, pipeline_input: PipelineInput) -> None:
321+
console.rule("[bold magenta]SecNode Legacy Pipeline")
306322
api_structure, tests = await build_pipeline_artifacts(pipeline_input)
307323
if not tests:
308324
logger.error("No tests generated. Aborting.")
309325
raise SystemExit(1)
310326

327+
console.rule("[bold cyan]Executing Generated Tests")
311328
results = await execute_proactive_tests(
312329
test_cases=tests,
313330
base_url=api_structure.base_url,
@@ -316,6 +333,8 @@ async def _run_full_pipeline(args, pipeline_input: PipelineInput) -> None:
316333
proxy=pipeline_input.proxy,
317334
verify_ssl=pipeline_input.verify_ssl,
318335
)
336+
337+
console.rule("[bold yellow]Validating Execution Results")
319338
findings = await validate_and_retest(
320339
results=results,
321340
base_url=api_structure.base_url,
@@ -329,11 +348,16 @@ async def _run_full_pipeline(args, pipeline_input: PipelineInput) -> None:
329348

330349
report = build_report(api_structure.title, findings)
331350
output_dir = write_report(report, args.target)
351+
352+
console.rule("[bold green]Scan Completed")
332353
logger.info("SecNode pipeline completed successfully", output_dir=output_dir)
333354

334355

335356
async def _run_agent_mode(args, pipeline_input: PipelineInput) -> None:
357+
console.rule("[bold magenta]SecNode Autonomous Agent Mode")
336358
api_structure, confirmed, suspected, metrics = await run_agent_pipeline(pipeline_input)
359+
360+
console.rule("[bold green]Scan Completed")
337361
report = build_report_with_context(
338362
api_structure.title,
339363
confirmed,

src/secnodeapi/services/pipeline.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -272,13 +272,18 @@ def _merge_unique_findings(existing: List[Finding], new_findings: List[Finding])
272272
return list(by_key.values())
273273

274274

275+
from rich.console import Console
276+
277+
console = Console()
278+
275279
async def run_agent_pipeline(
276280
pipeline_input: PipelineInput,
277281
) -> Tuple[SchemaStructure, List[Finding], List[Finding], Dict[str, int]]:
278282
"""
279283
Autonomous runtime loop:
280284
plan -> execute -> observe -> validate -> replan.
281285
"""
286+
console.rule("[bold cyan]Phase 1: Building Execution Plan")
282287
api_structure, seed_tests = await build_pipeline_artifacts(pipeline_input)
283288
identities = _resolve_identities(pipeline_input)
284289
queue = _deduplicate_test_cases(
@@ -294,12 +299,14 @@ async def run_agent_pipeline(
294299

295300
while queue and remaining_budget > 0 and iteration < pipeline_input.max_iterations:
296301
iteration += 1
302+
console.rule(f"[bold magenta]Phase 2: Agent Iteration {iteration}")
297303
batch, queue = _clip_for_budget(
298304
queue, remaining_budget, pipeline_input.per_endpoint_budget
299305
)
300306
if not batch:
301307
break
302-
308+
309+
console.print(f"[dim]Executing batch of {len(batch)} tests. {len(queue)} remaining in queue.[/]")
303310
results, stats = await execute_proactive_tests_detailed(
304311
test_cases=batch,
305312
base_url=api_structure.base_url,
@@ -328,6 +335,7 @@ async def run_agent_pipeline(
328335
queue = _deduplicate_test_cases(queue + chain_tests)
329336

330337
# Run final AI deduplication
338+
console.rule("[bold yellow]Phase 3: Final Intelligence Processing")
331339
confirmed = await deduplicate_findings_with_ai(confirmed)
332340

333341
metrics = {

0 commit comments

Comments
 (0)