codesecure-cli 1.0.0b10__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- codesecure_cli-1.0.0b10/PKG-INFO +74 -0
- codesecure_cli-1.0.0b10/README.md +63 -0
- codesecure_cli-1.0.0b10/pyproject.toml +26 -0
- codesecure_cli-1.0.0b10/setup.cfg +4 -0
- codesecure_cli-1.0.0b10/setup.py +5 -0
- codesecure_cli-1.0.0b10/src/codesecure_cli/__init__.py +9 -0
- codesecure_cli-1.0.0b10/src/codesecure_cli/feedback.py +61 -0
- codesecure_cli-1.0.0b10/src/codesecure_cli/main.py +786 -0
- codesecure_cli-1.0.0b10/src/codesecure_cli/setup.py +95 -0
- codesecure_cli-1.0.0b10/src/codesecure_cli.egg-info/PKG-INFO +74 -0
- codesecure_cli-1.0.0b10/src/codesecure_cli.egg-info/SOURCES.txt +13 -0
- codesecure_cli-1.0.0b10/src/codesecure_cli.egg-info/dependency_links.txt +1 -0
- codesecure_cli-1.0.0b10/src/codesecure_cli.egg-info/entry_points.txt +2 -0
- codesecure_cli-1.0.0b10/src/codesecure_cli.egg-info/requires.txt +3 -0
- codesecure_cli-1.0.0b10/src/codesecure_cli.egg-info/top_level.txt +1 -0
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: codesecure-cli
|
|
3
|
+
Version: 1.0.0b10
|
|
4
|
+
Summary: CodeSecure Command Line Interface
|
|
5
|
+
License: MIT
|
|
6
|
+
Requires-Python: >=3.10
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
Requires-Dist: codesecure-core
|
|
9
|
+
Requires-Dist: click>=8.0.0
|
|
10
|
+
Requires-Dist: rich>=13.0.0
|
|
11
|
+
|
|
12
|
+
# CodeSecure CLI (`codesecure-cli`)
|
|
13
|
+
|
|
14
|
+
The `codesecure-cli` package is the user-facing terminal interface for the CodeSecure monorepo. It wraps the functionality of the core orchestration engine within an interactive, rich terminal application.
|
|
15
|
+
|
|
16
|
+
## šÆ Module Purpose
|
|
17
|
+
|
|
18
|
+
This package focuses entirely on **Developer Experience (DX)** inside the terminal and CI/CD environments. It parses CLI arguments using `Click`, dynamically structures interactive menus with `Rich`, and connects seamlessly to the Python business logic embedded in `codesecure-core`.
|
|
19
|
+
|
|
20
|
+
*Note: The CLI is designed to be a "Thin Wrapper". Core scanning logic does not exist in this package.*
|
|
21
|
+
|
|
22
|
+
## š¦ Local Installation
|
|
23
|
+
|
|
24
|
+
Installing the CLI relies on standard Python setups but requires the workspace dependency mapped to `codesecure-core`.
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
cd packages/cli
|
|
28
|
+
python -m venv .venv
|
|
29
|
+
source .venv/bin/activate
|
|
30
|
+
|
|
31
|
+
# Recommended: Install via pip from the project root rather than locally
|
|
32
|
+
# This dynamically resolves workspace dependencies
|
|
33
|
+
cd ../../
|
|
34
|
+
pip install -e ./packages/cli
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## š Exported Commands & Features
|
|
38
|
+
|
|
39
|
+
The entry point script installs a global binary command: `codesecure`. Check the top-level commands below.
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
# Validates the active scanners provided by Core
|
|
43
|
+
codesecure list-scanners
|
|
44
|
+
|
|
45
|
+
# Initializes Beta agreements
|
|
46
|
+
codesecure init
|
|
47
|
+
|
|
48
|
+
# Prompts setup for Gemini or Kiro CLI
|
|
49
|
+
codesecure login
|
|
50
|
+
|
|
51
|
+
# The main workhorse: Initiates an interactive async scan on a target path
|
|
52
|
+
codesecure scan ./my-project --provider kiro --output json,html
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## š ļø Integration Example
|
|
56
|
+
|
|
57
|
+
Since the CLI is a consumption edge-node in the monorepo architecture, its integrations primarily concern reading from `core`. Below is a snippet of how the CLI bypasses MCP overhead to list scanners efficiently from the programmatic core.
|
|
58
|
+
|
|
59
|
+
```python
|
|
60
|
+
import click
|
|
61
|
+
from codesecure.common.models import ScanMode
|
|
62
|
+
|
|
63
|
+
@click.command()
|
|
64
|
+
def list_scanners():
|
|
65
|
+
"""List available security scanners from Core."""
|
|
66
|
+
from codesecure.scanners.engine import get_scanner_engine
|
|
67
|
+
engine = get_scanner_engine()
|
|
68
|
+
|
|
69
|
+
try:
|
|
70
|
+
local_scanners = engine.get_available_scanners(ScanMode.LOCAL)
|
|
71
|
+
print(f"Available Local Scanners: {', '.join(local_scanners)}")
|
|
72
|
+
except Exception as e:
|
|
73
|
+
print(f"Failed to list scanners: {e}")
|
|
74
|
+
```
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# CodeSecure CLI (`codesecure-cli`)
|
|
2
|
+
|
|
3
|
+
The `codesecure-cli` package is the user-facing terminal interface for the CodeSecure monorepo. It wraps the functionality of the core orchestration engine within an interactive, rich terminal application.
|
|
4
|
+
|
|
5
|
+
## šÆ Module Purpose
|
|
6
|
+
|
|
7
|
+
This package focuses entirely on **Developer Experience (DX)** inside the terminal and CI/CD environments. It parses CLI arguments using `Click`, dynamically structures interactive menus with `Rich`, and connects seamlessly to the Python business logic embedded in `codesecure-core`.
|
|
8
|
+
|
|
9
|
+
*Note: The CLI is designed to be a "Thin Wrapper". Core scanning logic does not exist in this package.*
|
|
10
|
+
|
|
11
|
+
## š¦ Local Installation
|
|
12
|
+
|
|
13
|
+
Installing the CLI relies on standard Python setups but requires the workspace dependency mapped to `codesecure-core`.
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
cd packages/cli
|
|
17
|
+
python -m venv .venv
|
|
18
|
+
source .venv/bin/activate
|
|
19
|
+
|
|
20
|
+
# Recommended: Install via pip from the project root rather than locally
|
|
21
|
+
# This dynamically resolves workspace dependencies
|
|
22
|
+
cd ../../
|
|
23
|
+
pip install -e ./packages/cli
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## š Exported Commands & Features
|
|
27
|
+
|
|
28
|
+
The entry point script installs a global binary command: `codesecure`. Check the top-level commands below.
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
# Validates the active scanners provided by Core
|
|
32
|
+
codesecure list-scanners
|
|
33
|
+
|
|
34
|
+
# Initializes Beta agreements
|
|
35
|
+
codesecure init
|
|
36
|
+
|
|
37
|
+
# Prompts setup for Gemini or Kiro CLI
|
|
38
|
+
codesecure login
|
|
39
|
+
|
|
40
|
+
# The main workhorse: Initiates an interactive async scan on a target path
|
|
41
|
+
codesecure scan ./my-project --provider kiro --output json,html
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## š ļø Integration Example
|
|
45
|
+
|
|
46
|
+
Since the CLI is a consumption edge-node in the monorepo architecture, its integrations primarily concern reading from `core`. Below is a snippet of how the CLI bypasses MCP overhead to list scanners efficiently from the programmatic core.
|
|
47
|
+
|
|
48
|
+
```python
|
|
49
|
+
import click
|
|
50
|
+
from codesecure.common.models import ScanMode
|
|
51
|
+
|
|
52
|
+
@click.command()
|
|
53
|
+
def list_scanners():
|
|
54
|
+
"""List available security scanners from Core."""
|
|
55
|
+
from codesecure.scanners.engine import get_scanner_engine
|
|
56
|
+
engine = get_scanner_engine()
|
|
57
|
+
|
|
58
|
+
try:
|
|
59
|
+
local_scanners = engine.get_available_scanners(ScanMode.LOCAL)
|
|
60
|
+
print(f"Available Local Scanners: {', '.join(local_scanners)}")
|
|
61
|
+
except Exception as e:
|
|
62
|
+
print(f"Failed to list scanners: {e}")
|
|
63
|
+
```
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61.0", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "codesecure-cli"
|
|
7
|
+
version = "1.0.0b10"
|
|
8
|
+
description = "CodeSecure Command Line Interface"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
license = {text = "MIT"}
|
|
12
|
+
dependencies = [
|
|
13
|
+
"codesecure-core",
|
|
14
|
+
"click>=8.0.0",
|
|
15
|
+
"rich>=13.0.0",
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
[project.scripts]
|
|
19
|
+
codesecure = "codesecure_cli.main:main_entry"
|
|
20
|
+
|
|
21
|
+
[tool.setuptools.packages.find]
|
|
22
|
+
where = ["src"]
|
|
23
|
+
include = ["codesecure_cli*"]
|
|
24
|
+
|
|
25
|
+
[tool.uv.sources]
|
|
26
|
+
codesecure-core = { workspace = true }
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
|
|
2
|
+
import click
|
|
3
|
+
from rich.console import Console
|
|
4
|
+
from rich.prompt import Prompt, Confirm
|
|
5
|
+
from rich.panel import Panel
|
|
6
|
+
|
|
7
|
+
from codesecure.telemetry.client import TelemetryClient
|
|
8
|
+
|
|
9
|
+
# Define local helpers to avoid circular import with main.py
|
|
10
|
+
console = Console()
|
|
11
|
+
|
|
12
|
+
def print_success(message: str):
|
|
13
|
+
"""Print success message."""
|
|
14
|
+
console.print(f"[green]ā {message}[/green]")
|
|
15
|
+
|
|
16
|
+
def print_error(message: str):
|
|
17
|
+
"""Print error message."""
|
|
18
|
+
console.print(f"[red]ā {message}[/red]")
|
|
19
|
+
|
|
20
|
+
def print_info(message: str):
|
|
21
|
+
"""Print info message."""
|
|
22
|
+
console.print(f"[cyan]ā {message}[/cyan]")
|
|
23
|
+
|
|
24
|
+
@click.command()
|
|
25
|
+
def feedback():
|
|
26
|
+
"""
|
|
27
|
+
Share feedback or report bugs to the CodeSecure team.
|
|
28
|
+
|
|
29
|
+
We value your input! Usage data and feedback help us improve CodeSecure.
|
|
30
|
+
"""
|
|
31
|
+
console.print(Panel(
|
|
32
|
+
"[bold cyan]CodeSecure Feedback[/bold cyan]\n"
|
|
33
|
+
"Help us improve by sharing your thoughts, feature requests, or bug reports.",
|
|
34
|
+
border_style="cyan"
|
|
35
|
+
))
|
|
36
|
+
|
|
37
|
+
# 1. Collect Feedback
|
|
38
|
+
user_feedback = Prompt.ask("\n[bold]What would you like to tell us?[/bold]")
|
|
39
|
+
|
|
40
|
+
if not user_feedback.strip():
|
|
41
|
+
print_error("Feedback cannot be empty.")
|
|
42
|
+
return
|
|
43
|
+
|
|
44
|
+
# 2. Optional Email
|
|
45
|
+
email = Prompt.ask("[bold]Email (optional, for follow-up)[/bold]", default="")
|
|
46
|
+
|
|
47
|
+
# 3. Confirm and Send
|
|
48
|
+
if Confirm.ask("\nSend this feedback to the CodeSecure team?", default=True, console=console):
|
|
49
|
+
# Instantiate client only when needed
|
|
50
|
+
client = TelemetryClient()
|
|
51
|
+
|
|
52
|
+
with console.status("[bold green]Sending feedback...", spinner="dots"):
|
|
53
|
+
# Mocking the call or actual call
|
|
54
|
+
success = client.send_feedback(improvement=user_feedback, email=email if email else None)
|
|
55
|
+
|
|
56
|
+
if success:
|
|
57
|
+
print_success("Thank you! Your feedback has been received.")
|
|
58
|
+
else:
|
|
59
|
+
print_error("Failed to send feedback. Please try again later.")
|
|
60
|
+
else:
|
|
61
|
+
print_info("Feedback cancelled.")
|
|
@@ -0,0 +1,786 @@
|
|
|
1
|
+
"""
|
|
2
|
+
CodeSecure CLI - Cross-Platform Security Scanner.
|
|
3
|
+
|
|
4
|
+
Cross-platform CLI for Windows, Mac, and Linux.
|
|
5
|
+
PRD Section 5.1: IDE Integration - "Direct Python"
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import asyncio
|
|
9
|
+
import logging
|
|
10
|
+
import sys
|
|
11
|
+
import traceback
|
|
12
|
+
from datetime import datetime, timezone
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import List, Optional
|
|
15
|
+
|
|
16
|
+
import click
|
|
17
|
+
|
|
18
|
+
from codesecure import __version__
|
|
19
|
+
|
|
20
|
+
# Removed direct ScannerEngine imports to enforce Layered Isolation
|
|
21
|
+
from codesecure.common.logging import get_logger
|
|
22
|
+
from codesecure.common.models import ScanMode, JobStatus
|
|
23
|
+
from codesecure.common.cloud_provider import CloudProvider, PROVIDER_CAPABILITIES, ProviderAvailability, SUPPORTED_PROVIDERS
|
|
24
|
+
from codesecure.common.config import is_terms_accepted, accept_terms
|
|
25
|
+
from codesecure.reports.generator import get_report_generator, ReportMetadata
|
|
26
|
+
from codesecure_cli.feedback import feedback
|
|
27
|
+
from codesecure.telemetry.client import TelemetryClient
|
|
28
|
+
|
|
29
|
+
# MCP Imports
|
|
30
|
+
import json
|
|
31
|
+
from mcp import ClientSession, StdioServerParameters
|
|
32
|
+
from mcp.client.stdio import stdio_client
|
|
33
|
+
|
|
34
|
+
# Rich Imports
|
|
35
|
+
from rich.console import Console
|
|
36
|
+
from rich.progress import (
|
|
37
|
+
Progress,
|
|
38
|
+
SpinnerColumn,
|
|
39
|
+
TextColumn,
|
|
40
|
+
BarColumn,
|
|
41
|
+
TaskProgressColumn,
|
|
42
|
+
TimeElapsedColumn
|
|
43
|
+
)
|
|
44
|
+
from rich.live import Live
|
|
45
|
+
from rich.panel import Panel
|
|
46
|
+
from rich.prompt import Confirm
|
|
47
|
+
|
|
48
|
+
console = Console()
|
|
49
|
+
|
|
50
|
+
# Logger initialization
|
|
51
|
+
logger = get_logger(__name__)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def print_banner():
|
|
55
|
+
"""Print CodeSecure banner."""
|
|
56
|
+
banner_content = "[bold deep_sky_blue1]š CodeSecure Scanner[/bold deep_sky_blue1]\n[dim white]Enterprise-Grade Security Analysis Platform[/dim white]"
|
|
57
|
+
|
|
58
|
+
panel = Panel(
|
|
59
|
+
banner_content,
|
|
60
|
+
subtitle="[bold gold1]BETA RELEASE[/bold gold1]",
|
|
61
|
+
subtitle_align="right",
|
|
62
|
+
border_style="bright_cyan",
|
|
63
|
+
padding=(1, 4),
|
|
64
|
+
expand=False
|
|
65
|
+
)
|
|
66
|
+
console.print(panel)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def print_success(message: str):
|
|
70
|
+
"""Print success message."""
|
|
71
|
+
console.print(f"[bold spring_green3]ā[/bold spring_green3] [white]{message}[/white]")
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def print_error(message: str):
|
|
75
|
+
"""Print error message."""
|
|
76
|
+
console.print(f"[bold red]ā[/bold red] [orange_red1]{message}[/orange_red1]")
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def print_info(message: str):
|
|
80
|
+
"""Print info message."""
|
|
81
|
+
console.print(f"[bold sky_blue1]ā[/bold sky_blue1] [grey85]{message}[/grey85]")
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def print_warning(message: str):
|
|
85
|
+
"""Print warning message."""
|
|
86
|
+
console.print(f"[bold gold1]ā {message}[/bold gold1]")
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def show_beta_terms():
|
|
90
|
+
"""Show Beta terms and ask for acceptance."""
|
|
91
|
+
console.print("\n[bold white]Welcome to the CodeSecure CLI Beta![/bold white]")
|
|
92
|
+
console.print("ā" * 50)
|
|
93
|
+
console.print("[bold yellow][!] This is pre-release software. Use at your own risk.[/bold yellow]")
|
|
94
|
+
console.print("[bold green][ā] Privacy First: This tool collects ZERO telemetry or usage data.[/bold green]")
|
|
95
|
+
console.print("\nBy continuing, you agree to our Beta Terms: [blue underline]https://codesecure.dev/beta-terms[/blue underline]")
|
|
96
|
+
console.print()
|
|
97
|
+
|
|
98
|
+
if Confirm.ask("Do you accept these terms?", default=False, console=console):
|
|
99
|
+
accept_terms()
|
|
100
|
+
print_success("Terms accepted. Welcome aboard!")
|
|
101
|
+
return True
|
|
102
|
+
else:
|
|
103
|
+
print_error("You must accept the terms to use CodeSecure CLI.")
|
|
104
|
+
return False
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
@click.group()
|
|
108
|
+
@click.version_option(version=__version__, prog_name="codesecure")
|
|
109
|
+
@click.pass_context
|
|
110
|
+
def cli(ctx):
|
|
111
|
+
"""
|
|
112
|
+
CodeSecure - Enterprise Security Analysis CLI (Beta).
|
|
113
|
+
|
|
114
|
+
Cross-platform security scanner for Windows, Mac, and Linux.
|
|
115
|
+
This product is currently in Beta and undergoing active development.
|
|
116
|
+
"""
|
|
117
|
+
# Skip terms check for help or version commands if possible
|
|
118
|
+
# In Click, help/version usually short-circuit before the group callback
|
|
119
|
+
# but we can check if a subcommand is about to be invoked.
|
|
120
|
+
if not is_terms_accepted():
|
|
121
|
+
# Only prompt if a subcommand is being invoked and it's not 'init'
|
|
122
|
+
if ctx.invoked_subcommand and ctx.invoked_subcommand != 'init':
|
|
123
|
+
if not show_beta_terms():
|
|
124
|
+
ctx.exit(0)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
@cli.command()
|
|
128
|
+
def init():
|
|
129
|
+
"""Initialize CodeSecure and accept Beta terms."""
|
|
130
|
+
print_banner()
|
|
131
|
+
if is_terms_accepted():
|
|
132
|
+
print_info("CodeSecure is already initialized and terms are accepted.")
|
|
133
|
+
else:
|
|
134
|
+
if show_beta_terms():
|
|
135
|
+
print_success("Initialization complete.")
|
|
136
|
+
else:
|
|
137
|
+
sys.exit(1)
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
@cli.command()
|
|
141
|
+
def login():
|
|
142
|
+
"""
|
|
143
|
+
Login to AI providers and CodeSecure services.
|
|
144
|
+
|
|
145
|
+
This command helps you authenticate with the AI providers used for enrichment.
|
|
146
|
+
"""
|
|
147
|
+
print_banner()
|
|
148
|
+
console.print("\n[bold sky_blue1]Authentication Guidance[/bold sky_blue1]")
|
|
149
|
+
console.print("ā" * 30)
|
|
150
|
+
|
|
151
|
+
console.print("\n[bold bright_cyan]Google Gemini (Default)[/bold bright_cyan]")
|
|
152
|
+
console.print(" 1. Ensure Google Cloud SDK is installed.")
|
|
153
|
+
console.print(" 2. Run: [bold white]gcloud auth login[/bold white]")
|
|
154
|
+
console.print(" 3. Run: [bold white]gcloud auth application-default login[/bold white]")
|
|
155
|
+
|
|
156
|
+
console.print("\n[bold bright_cyan]Kiro CLI (AWS)[/bold bright_cyan]")
|
|
157
|
+
console.print(" 1. Ensure Kiro CLI is installed.")
|
|
158
|
+
console.print(" 2. Run: [bold white]kiro-cli login[/bold white]")
|
|
159
|
+
|
|
160
|
+
console.print("\n[bold bright_cyan]Azure AI (Azure OpenAI)[/bold bright_cyan]")
|
|
161
|
+
console.print(" 1. Ensure Azure CLI is installed.")
|
|
162
|
+
console.print(" 2. Run: [bold white]az login[/bold white]")
|
|
163
|
+
console.print(" 3. Set Env: [bold white]$env:AZURE_OPENAI_ENDPOINT=\"https://...\"[/bold white]")
|
|
164
|
+
console.print(" 4. Set Env: [bold white]$env:AZURE_OPENAI_DEPLOYMENT=\"gpt-4o\"[/bold white]")
|
|
165
|
+
|
|
166
|
+
console.print("\n[bold bright_cyan]OpenAI (GPT-4o / GPT-4o-mini)[/bold bright_cyan]")
|
|
167
|
+
console.print(" 1. Get an API key from [bold white]https://platform.openai.com/api-keys[/bold white]")
|
|
168
|
+
console.print(" 2. Set Env: [bold white]$env:OPENAI_API_KEY=\"sk-...\"[/bold white]")
|
|
169
|
+
console.print(" 3. (Optional) Set Env: [bold white]$env:OPENAI_MODEL=\"gpt-4o-mini\"[/bold white]")
|
|
170
|
+
console.print(" 4. (Optional) Set Env: [bold white]$env:OPENAI_BASE_URL=\"https://...\"[/bold white] for custom endpoints")
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
@cli.command(name="mcp-server", hidden=True)
|
|
176
|
+
def mcp_server_command():
|
|
177
|
+
"""Internal command to start the MCP server."""
|
|
178
|
+
# Import here to avoid circular dependencies
|
|
179
|
+
from codesecure_mcp.scanner.server import main as server_main
|
|
180
|
+
server_main()
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
@cli.command()
|
|
184
|
+
@click.argument("path", type=click.Path(exists=True, path_type=Path))
|
|
185
|
+
@click.option(
|
|
186
|
+
"--format", "-f",
|
|
187
|
+
type=click.Choice(["html", "json", "sarif", "markdown", "all"]),
|
|
188
|
+
multiple=True,
|
|
189
|
+
default=["all"],
|
|
190
|
+
help="Output report format (default: all)"
|
|
191
|
+
)
|
|
192
|
+
@click.option(
|
|
193
|
+
"--output", "-o",
|
|
194
|
+
type=click.Path(path_type=Path),
|
|
195
|
+
default=None,
|
|
196
|
+
help="Output directory for reports (default: 'codesecure_scan_reports' in the scanned directory)"
|
|
197
|
+
)
|
|
198
|
+
@click.option(
|
|
199
|
+
"--scanners", "-s",
|
|
200
|
+
multiple=True,
|
|
201
|
+
help="Specific scanners to run (default: all available)"
|
|
202
|
+
)
|
|
203
|
+
@click.option(
|
|
204
|
+
"--mode", "-m",
|
|
205
|
+
type=click.Choice(["local", "container"]),
|
|
206
|
+
default="local",
|
|
207
|
+
help="Execution mode (default: local)"
|
|
208
|
+
)
|
|
209
|
+
@click.option(
|
|
210
|
+
"--timeout", "-t",
|
|
211
|
+
type=int,
|
|
212
|
+
default=480,
|
|
213
|
+
help="Timeout per scanner in seconds (default: 480)"
|
|
214
|
+
)
|
|
215
|
+
@click.option(
|
|
216
|
+
"--ai-provider",
|
|
217
|
+
type=click.Choice(["google", "openai", "anthropic", "aws", "azure", "none"], case_sensitive=False),
|
|
218
|
+
default=None,
|
|
219
|
+
help="AI provider override for explicit false positive filtering (e.g., 'openai')"
|
|
220
|
+
)
|
|
221
|
+
@click.option(
|
|
222
|
+
"--ai-api-key",
|
|
223
|
+
type=str,
|
|
224
|
+
default=None,
|
|
225
|
+
envvar='CODESECURE_AI_API_KEY',
|
|
226
|
+
help="API Key for the chosen AI provider (also via CODESECURE_AI_API_KEY)"
|
|
227
|
+
)
|
|
228
|
+
@click.option(
|
|
229
|
+
"--fail-on", "-F",
|
|
230
|
+
type=str,
|
|
231
|
+
default=None,
|
|
232
|
+
help="Comma-separated list of severities to fail the pipeline (e.g., 'critical,high')"
|
|
233
|
+
)
|
|
234
|
+
def scan(
|
|
235
|
+
path: Path,
|
|
236
|
+
format: tuple,
|
|
237
|
+
output: Optional[Path],
|
|
238
|
+
scanners: tuple,
|
|
239
|
+
mode: str,
|
|
240
|
+
timeout: int,
|
|
241
|
+
ai_provider: Optional[str] = None,
|
|
242
|
+
ai_api_key: Optional[str] = None,
|
|
243
|
+
fail_on: Optional[str] = None,
|
|
244
|
+
):
|
|
245
|
+
"""
|
|
246
|
+
Run security scan on target path.
|
|
247
|
+
|
|
248
|
+
PATH: Directory or file to scan.
|
|
249
|
+
|
|
250
|
+
Example:
|
|
251
|
+
codesecure scan ./my-project --format html
|
|
252
|
+
"""
|
|
253
|
+
print_banner()
|
|
254
|
+
|
|
255
|
+
start_time = datetime.now()
|
|
256
|
+
console.print(f"\n[bold white]š Scan Initiated:[/bold white] [bright_cyan]{start_time.strftime('%Y-%m-%d %H:%M:%S')}[/bright_cyan]")
|
|
257
|
+
console.print("ā" * 50)
|
|
258
|
+
|
|
259
|
+
# Silence noise from report generator
|
|
260
|
+
logging.getLogger("codesecure.reports.generator").setLevel(logging.WARNING)
|
|
261
|
+
|
|
262
|
+
# Silence noise from MCP/Starlette/HTTPX
|
|
263
|
+
logging.getLogger("mcp").setLevel(logging.WARNING)
|
|
264
|
+
logging.getLogger("fastmcp").setLevel(logging.WARNING)
|
|
265
|
+
logging.getLogger("starlette").setLevel(logging.WARNING)
|
|
266
|
+
logging.getLogger("uvicorn").setLevel(logging.WARNING)
|
|
267
|
+
logging.getLogger("httpcore").setLevel(logging.WARNING)
|
|
268
|
+
logging.getLogger("httpx").setLevel(logging.WARNING)
|
|
269
|
+
|
|
270
|
+
# Set base output directory
|
|
271
|
+
if output:
|
|
272
|
+
base_output_dir = output
|
|
273
|
+
else:
|
|
274
|
+
# Default to 'codesecure_scan_reports' relative to the path being scanned
|
|
275
|
+
base_path = path if path.is_dir() else path.parent
|
|
276
|
+
base_output_dir = base_path / "codesecure_scan_reports"
|
|
277
|
+
|
|
278
|
+
base_output_dir.mkdir(parents=True, exist_ok=True)
|
|
279
|
+
|
|
280
|
+
# Parse mode
|
|
281
|
+
scan_mode = ScanMode.LOCAL if mode == "local" else ScanMode.CONTAINER
|
|
282
|
+
|
|
283
|
+
# Parse scanners
|
|
284
|
+
scanner_list = list(scanners) if scanners else None
|
|
285
|
+
|
|
286
|
+
# Interactive provider selection if not provided
|
|
287
|
+
if ai_provider is None:
|
|
288
|
+
console.print("\n[bold cyan]š¤ AI Enrichment Options[/bold cyan]")
|
|
289
|
+
console.print("CodeSecure can use AI to provide deeper security insights, remediation guidance, and false positive reduction.")
|
|
290
|
+
|
|
291
|
+
if Confirm.ask("Would you like to enable AI-powered analysis?", default=False, console=console):
|
|
292
|
+
console.print("\n[bold white]Available AI Providers & Pre-requisites:[/bold white]")
|
|
293
|
+
console.print(" [bold blue][1] Google Gemini (Primary)[/bold blue]")
|
|
294
|
+
console.print(" ⢠Install: 'npm install -g @google/gemini-cli'")
|
|
295
|
+
console.print(" ⢠Authenticate: 'gcloud auth login'")
|
|
296
|
+
|
|
297
|
+
console.print(" [bold blue][2] Kiro CLI (AWS)[/bold blue]")
|
|
298
|
+
console.print(" ⢠Install: Kiro CLI (https://kiro.ai/docs)")
|
|
299
|
+
console.print(" ⢠Authenticate: 'kiro-cli login'")
|
|
300
|
+
|
|
301
|
+
console.print(" [bold blue][3] Azure AI (Enterprise)[/bold blue]")
|
|
302
|
+
console.print(" ⢠Pre-req: Azure CLI + 'az login'")
|
|
303
|
+
console.print(" ⢠Config: Set AZURE_OPENAI_ENDPOINT & AZURE_OPENAI_DEPLOYMENT")
|
|
304
|
+
|
|
305
|
+
choice = click.prompt("\nSelect a provider (or 0 to cancel)", type=click.Choice(["0", "1", "2", "3"]), default="1")
|
|
306
|
+
|
|
307
|
+
if choice == "0":
|
|
308
|
+
print_info("AI enrichment declined. Proceeding with None.")
|
|
309
|
+
cloud_provider = CloudProvider.NONE
|
|
310
|
+
else:
|
|
311
|
+
mapping = {"1": CloudProvider.GOOGLE, "2": CloudProvider.AWS, "3": CloudProvider.AZURE}
|
|
312
|
+
cloud_provider = mapping[choice]
|
|
313
|
+
console.print(f"\n[bold green]Selected: {cloud_provider.value.upper()}[/bold green]")
|
|
314
|
+
else:
|
|
315
|
+
print_info("Proceeding with AI features disabled (None).")
|
|
316
|
+
cloud_provider = CloudProvider.NONE
|
|
317
|
+
else:
|
|
318
|
+
cloud_provider = CloudProvider(ai_provider)
|
|
319
|
+
|
|
320
|
+
# Validate provider availability if not None
|
|
321
|
+
if cloud_provider != CloudProvider.NONE:
|
|
322
|
+
status = ProviderAvailability.check_provider(cloud_provider, check_quota=True)
|
|
323
|
+
if not status["available"]:
|
|
324
|
+
console.print(f"\n[bold yellow]ā AI Provider '{cloud_provider.value}' not ready: {status['error']}[/bold yellow]")
|
|
325
|
+
if Confirm.ask("Would you like to proceed with the scan without AI enrichment?", default=False, console=console):
|
|
326
|
+
print_info("Proceeding with AI capabilities disabled.")
|
|
327
|
+
cloud_provider = CloudProvider.NONE
|
|
328
|
+
ai_provider = None
|
|
329
|
+
else:
|
|
330
|
+
print_error("Scan aborted. Please configure the AI provider and try again.")
|
|
331
|
+
sys.exit(0)
|
|
332
|
+
|
|
333
|
+
# Parse formats
|
|
334
|
+
formats_to_gen = list(format)
|
|
335
|
+
if "all" in formats_to_gen:
|
|
336
|
+
formats_to_gen = ["html", "json", "sarif", "markdown"]
|
|
337
|
+
|
|
338
|
+
# Set scan output directory
|
|
339
|
+
scan_output_dir = base_output_dir / f"scan_{start_time.strftime('%Y%m%d_%H%M%S')}"
|
|
340
|
+
scan_output_dir.mkdir(parents=True, exist_ok=True)
|
|
341
|
+
|
|
342
|
+
# Provider capabilities for UI
|
|
343
|
+
caps = PROVIDER_CAPABILITIES.get(cloud_provider)
|
|
344
|
+
|
|
345
|
+
# UI display string for AI Provider override
|
|
346
|
+
effective_ai_display = ai_provider.upper() if ai_provider else (
|
|
347
|
+
cloud_provider.value.upper() if cloud_provider != CloudProvider.NONE else "None"
|
|
348
|
+
)
|
|
349
|
+
|
|
350
|
+
# Show configuration
|
|
351
|
+
conf_content = (
|
|
352
|
+
f"[bold sky_blue1]Target:[/bold sky_blue1] [blue]{path.resolve()}[/blue]\n"
|
|
353
|
+
f"[bold sky_blue1]Mode:[/bold sky_blue1] [white]{mode.capitalize()}[/white]\n"
|
|
354
|
+
f"[bold sky_blue1]Output:[/bold sky_blue1] [white]{', '.join([f.upper() for f in formats_to_gen])}[/white]\n"
|
|
355
|
+
f"[bold sky_blue1]Provider:[/bold sky_blue1] [bright_cyan]{effective_ai_display}[/bright_cyan]\n"
|
|
356
|
+
f"[dim]AI Review: {'ā
' if caps.code_review_enabled or ai_provider else 'ā'} | FP Detection: {'ā
' if caps.false_positive_detection or ai_provider else 'ā'}[/dim]"
|
|
357
|
+
)
|
|
358
|
+
|
|
359
|
+
console.print(Panel(conf_content, title="[bold white]Scan Configuration[/bold white]", border_style="bright_cyan", expand=False))
|
|
360
|
+
console.print()
|
|
361
|
+
|
|
362
|
+
# Run scan
|
|
363
|
+
try:
|
|
364
|
+
result = asyncio.run(_run_scan(
|
|
365
|
+
path,
|
|
366
|
+
scanner_list,
|
|
367
|
+
scan_mode,
|
|
368
|
+
timeout,
|
|
369
|
+
cloud_provider,
|
|
370
|
+
ai_provider_override=ai_provider,
|
|
371
|
+
api_key_override=ai_api_key
|
|
372
|
+
))
|
|
373
|
+
except Exception as e:
|
|
374
|
+
print_error(f"Scan failed: {e}")
|
|
375
|
+
sys.exit(1)
|
|
376
|
+
|
|
377
|
+
# Evaluate Quality Gate
|
|
378
|
+
gate_failed = False
|
|
379
|
+
if fail_on:
|
|
380
|
+
thresholds = [s.strip().lower() for s in fail_on.split(",")]
|
|
381
|
+
for sev in thresholds:
|
|
382
|
+
if result.findings_by_severity.get(sev, 0) > 0:
|
|
383
|
+
gate_failed = True
|
|
384
|
+
break
|
|
385
|
+
|
|
386
|
+
end_time = datetime.now()
|
|
387
|
+
duration = end_time - start_time
|
|
388
|
+
|
|
389
|
+
console.print("ā" * 50)
|
|
390
|
+
console.print("\n[bold white]š Scan Summary[/bold white]")
|
|
391
|
+
console.print("ā" * 50)
|
|
392
|
+
|
|
393
|
+
# Show summary
|
|
394
|
+
if result.success:
|
|
395
|
+
files_suffix = f" across {result.scanned_files} files" if result.scanned_files > 0 else ""
|
|
396
|
+
if result.total_findings > 0:
|
|
397
|
+
print_success(f"Scan completed: {result.total_findings} findings identified{files_suffix}")
|
|
398
|
+
else:
|
|
399
|
+
print_success(f"Scan completed: No security issues found{files_suffix} š")
|
|
400
|
+
|
|
401
|
+
if result.errors:
|
|
402
|
+
print_warning(f"Note: {len(result.errors)} tool errors occurred during execution")
|
|
403
|
+
|
|
404
|
+
# Show severity breakdown
|
|
405
|
+
severity_counts = result.findings_by_severity
|
|
406
|
+
if result.total_findings > 0:
|
|
407
|
+
console.print("\n[dim]Findings Breakdown:[/dim]")
|
|
408
|
+
for sev in ["critical", "high", "medium", "low"]:
|
|
409
|
+
count = severity_counts.get(sev, 0)
|
|
410
|
+
if count > 0:
|
|
411
|
+
color = {"critical": "bold red", "high": "orange_red1", "medium": "gold1", "low": "deep_sky_blue1"}[sev]
|
|
412
|
+
console.print(f" [bold {color}]⢠{sev.upper():<8}:[/bold {color}] [white]{count}[/white]")
|
|
413
|
+
|
|
414
|
+
# Show errors for partial success
|
|
415
|
+
if result.errors:
|
|
416
|
+
console.print("\n[bold gold1]ā ļø Scanner Diagnostics[/bold gold1]")
|
|
417
|
+
for error in result.errors:
|
|
418
|
+
print_error(f" {error}")
|
|
419
|
+
|
|
420
|
+
else:
|
|
421
|
+
print_error(f"Scan failed with {len(result.errors)} errors")
|
|
422
|
+
for error in result.errors:
|
|
423
|
+
print_error(f" {error}")
|
|
424
|
+
|
|
425
|
+
# Evidence Location
|
|
426
|
+
console.print(f"\n[bold white]š Evidence Location[/bold white]")
|
|
427
|
+
console.print(f" [dim cyan]{scan_output_dir}[/dim cyan]")
|
|
428
|
+
|
|
429
|
+
# Generate reports
|
|
430
|
+
console.print(f"\n[bold white]š Security Reports Generated[/bold white]")
|
|
431
|
+
report_files = []
|
|
432
|
+
for fmt in formats_to_gen:
|
|
433
|
+
try:
|
|
434
|
+
report_path = _generate_report(result, fmt, scan_output_dir, path)
|
|
435
|
+
report_files.append((fmt.upper(), report_path.name))
|
|
436
|
+
except Exception as e:
|
|
437
|
+
print_error(f" ⢠{fmt.upper():<8} : Failed ({e})")
|
|
438
|
+
|
|
439
|
+
for fmt_name, filename in report_files:
|
|
440
|
+
console.print(f" ⢠[bold sky_blue1]{fmt_name:<10}:[/bold sky_blue1] [white]{filename}[/white]")
|
|
441
|
+
|
|
442
|
+
console.print("ā" * 50)
|
|
443
|
+
console.print(f"[bold white]š Scan Finished:[/bold white] [bright_cyan]{end_time.strftime('%Y-%m-%d %H:%M:%S')}[/bright_cyan]")
|
|
444
|
+
|
|
445
|
+
# Calculate minutes and seconds
|
|
446
|
+
mins, secs = divmod(int(duration.total_seconds()), 60)
|
|
447
|
+
duration_str = f"{mins}m {secs}s" if mins > 0 else f"{secs}s"
|
|
448
|
+
console.print(f"[bold spring_green3]ā±ļø Total Duration:[/bold spring_green3] [white]{duration_str}[/white]\n")
|
|
449
|
+
|
|
450
|
+
console.print("[dim italic cyan]š” Have feedback? Run 'codesecure feedback' to help us improve![/dim italic cyan]\n")
|
|
451
|
+
|
|
452
|
+
# Exit code based on Quality Gate
|
|
453
|
+
if gate_failed:
|
|
454
|
+
print_error(f"Quality Gate Failed: Found findings matching --fail-on thresholds ({fail_on})")
|
|
455
|
+
sys.exit(1)
|
|
456
|
+
|
|
457
|
+
|
|
458
|
+
async def _run_scan(
|
|
459
|
+
path: Path,
|
|
460
|
+
scanners: Optional[List[str]],
|
|
461
|
+
mode: ScanMode,
|
|
462
|
+
timeout: int,
|
|
463
|
+
cloud_provider: CloudProvider,
|
|
464
|
+
ai_provider_override: Optional[str] = None,
|
|
465
|
+
api_key_override: Optional[str] = None
|
|
466
|
+
):
|
|
467
|
+
"""Run the scan asynchronously via Direct Python Core API."""
|
|
468
|
+
from codesecure.scanners.engine import get_scanner_engine
|
|
469
|
+
from codesecure.jobs.manager import get_job_manager
|
|
470
|
+
from codesecure.ai_providers.manager import get_ai_manager
|
|
471
|
+
from rich.progress import Progress, SpinnerColumn, TextColumn, BarColumn, TaskProgressColumn, TimeElapsedColumn
|
|
472
|
+
|
|
473
|
+
# Initialize the overriding AI Manager before the engine starts
|
|
474
|
+
get_ai_manager(
|
|
475
|
+
cloud_provider=cloud_provider,
|
|
476
|
+
ai_provider_override=ai_provider_override,
|
|
477
|
+
api_key_override=api_key_override
|
|
478
|
+
)
|
|
479
|
+
|
|
480
|
+
engine = get_scanner_engine()
|
|
481
|
+
job_manager = get_job_manager()
|
|
482
|
+
|
|
483
|
+
print_info("Initializing Hexagonal Core...")
|
|
484
|
+
|
|
485
|
+
job_id_box = {"id": None}
|
|
486
|
+
|
|
487
|
+
async def scan_task():
|
|
488
|
+
for _ in range(50):
|
|
489
|
+
if job_id_box["id"]:
|
|
490
|
+
break
|
|
491
|
+
await asyncio.sleep(0.01)
|
|
492
|
+
|
|
493
|
+
job_id = job_id_box["id"]
|
|
494
|
+
|
|
495
|
+
async def progress_cb(pct, msg):
|
|
496
|
+
if job_id:
|
|
497
|
+
await job_manager.update_job(job_id, pct, status=JobStatus.RUNNING)
|
|
498
|
+
|
|
499
|
+
return await engine.run_scan_with_progress(
|
|
500
|
+
path, job_id, scanners, mode, timeout=timeout,
|
|
501
|
+
progress_callback=progress_cb, cloud_provider=cloud_provider
|
|
502
|
+
)
|
|
503
|
+
|
|
504
|
+
scan_job_id = await job_manager.create_job("scan", scan_task())
|
|
505
|
+
job_id_box["id"] = scan_job_id
|
|
506
|
+
|
|
507
|
+
click.echo(click.style("ā ", fg="cyan") + "Scan Engine Started")
|
|
508
|
+
|
|
509
|
+
with Progress(
|
|
510
|
+
SpinnerColumn(spinner_name="dots", style="cyan"),
|
|
511
|
+
TextColumn("[bold blue]{task.description}"),
|
|
512
|
+
BarColumn(bar_width=40, style="blue", complete_style="green"),
|
|
513
|
+
TaskProgressColumn(),
|
|
514
|
+
"ā¢",
|
|
515
|
+
TimeElapsedColumn(),
|
|
516
|
+
console=console
|
|
517
|
+
) as progress_bar:
|
|
518
|
+
scan_task_bar = progress_bar.add_task("Initializing...", total=100)
|
|
519
|
+
|
|
520
|
+
while True:
|
|
521
|
+
status_data = await job_manager.get_status(scan_job_id)
|
|
522
|
+
if not status_data:
|
|
523
|
+
await asyncio.sleep(0.5)
|
|
524
|
+
continue
|
|
525
|
+
|
|
526
|
+
progress_val = status_data.get("progress", 0)
|
|
527
|
+
state = status_data.get("state", "pending").lower()
|
|
528
|
+
|
|
529
|
+
if progress_val < 5:
|
|
530
|
+
desc = f"Initializing scan engine..."
|
|
531
|
+
elif progress_val < 15:
|
|
532
|
+
desc = f"Starting security tools..."
|
|
533
|
+
elif progress_val < 75:
|
|
534
|
+
desc = f"Running scanners..."
|
|
535
|
+
elif progress_val < 80:
|
|
536
|
+
desc = f"Merging findings..."
|
|
537
|
+
elif progress_val < 90:
|
|
538
|
+
desc = f"Enriching with AI analysis..."
|
|
539
|
+
else:
|
|
540
|
+
desc = f"Finalizing results..."
|
|
541
|
+
|
|
542
|
+
progress_bar.update(scan_task_bar, completed=progress_val, description=desc)
|
|
543
|
+
|
|
544
|
+
if state == "completed":
|
|
545
|
+
progress_bar.update(scan_task_bar, completed=100, description="[bold green]Scan completed successfully[/bold green]")
|
|
546
|
+
break
|
|
547
|
+
elif state in ["failed", "cancelled", "timeout"]:
|
|
548
|
+
error = status_data.get("error", "Unknown error")
|
|
549
|
+
progress_bar.update(scan_task_bar, description=f"[bold red]Scan {state}[/bold red]")
|
|
550
|
+
raise Exception(f"Scan failed: {error}")
|
|
551
|
+
|
|
552
|
+
await asyncio.sleep(0.5)
|
|
553
|
+
|
|
554
|
+
result = await job_manager.get_result(scan_job_id)
|
|
555
|
+
if not result:
|
|
556
|
+
raise Exception("Scan completed but no result was found.")
|
|
557
|
+
return result
|
|
558
|
+
|
|
559
|
+
|
|
560
|
+
def _get_available_scanners(mode: ScanMode) -> List[str]:
|
|
561
|
+
"""Get available scanners directly from the core engine."""
|
|
562
|
+
from codesecure.scanners.engine import get_scanner_engine
|
|
563
|
+
engine = get_scanner_engine()
|
|
564
|
+
return engine.get_available_scanners(mode)
|
|
565
|
+
|
|
566
|
+
|
|
567
|
+
def _generate_report(result, format: str, output_dir: Path, scan_target: Path) -> Path:
|
|
568
|
+
"""Generate report and return path."""
|
|
569
|
+
generator = get_report_generator()
|
|
570
|
+
|
|
571
|
+
metadata = ReportMetadata(
|
|
572
|
+
title="CodeSecure Security Scan Report",
|
|
573
|
+
scan_target=str(scan_target.resolve()),
|
|
574
|
+
scan_duration=result.duration_seconds,
|
|
575
|
+
job_id=result.job_id,
|
|
576
|
+
)
|
|
577
|
+
|
|
578
|
+
return generator.generate(result, format, output_dir, metadata)
|
|
579
|
+
|
|
580
|
+
|
|
581
|
+
@cli.command()
|
|
582
|
+
@click.argument("path", type=click.Path(exists=True, path_type=Path))
|
|
583
|
+
@click.option(
|
|
584
|
+
"--format", "-f",
|
|
585
|
+
type=click.Choice(["html", "json", "sarif", "markdown", "all"]),
|
|
586
|
+
multiple=True,
|
|
587
|
+
default=["all"],
|
|
588
|
+
help="Output report format (default: all)"
|
|
589
|
+
)
|
|
590
|
+
@click.option(
|
|
591
|
+
"--output", "-o",
|
|
592
|
+
type=click.Path(path_type=Path),
|
|
593
|
+
default=None,
|
|
594
|
+
help="Output directory for reports (default: 'codesecure_reports' in the scanned directory)"
|
|
595
|
+
)
|
|
596
|
+
def report(path: Path, format: tuple, output: Optional[Path]):
|
|
597
|
+
"""
|
|
598
|
+
Generate report from previous scan results.
|
|
599
|
+
|
|
600
|
+
PATH: Path to a previously generated CodeSecure JSON report file.
|
|
601
|
+
|
|
602
|
+
Example:
|
|
603
|
+
codesecure report ./codesecure_scan_reports/scan_1/report.json --format html
|
|
604
|
+
"""
|
|
605
|
+
print_banner()
|
|
606
|
+
|
|
607
|
+
start_time = datetime.now()
|
|
608
|
+
console.print(f"\n[bold white]š Report Generation Initiated:[/bold white] [bright_cyan]{start_time.strftime('%Y-%m-%d %H:%M:%S')}[/bright_cyan]")
|
|
609
|
+
console.print("ā" * 50)
|
|
610
|
+
|
|
611
|
+
if path.is_dir() or not path.name.endswith(".json"):
|
|
612
|
+
print_error("PATH must be a .json file generated by a previous CodeSecure scan.")
|
|
613
|
+
sys.exit(1)
|
|
614
|
+
|
|
615
|
+
formats_to_gen = list(format)
|
|
616
|
+
if "all" in formats_to_gen:
|
|
617
|
+
formats_to_gen = ["html", "json", "sarif", "markdown"]
|
|
618
|
+
|
|
619
|
+
if output:
|
|
620
|
+
base_output_dir = output
|
|
621
|
+
else:
|
|
622
|
+
base_output_dir = path.parent
|
|
623
|
+
|
|
624
|
+
base_output_dir.mkdir(parents=True, exist_ok=True)
|
|
625
|
+
|
|
626
|
+
# Show configuration
|
|
627
|
+
conf_content = (
|
|
628
|
+
f"[bold sky_blue1]Input JSON:[/bold sky_blue1] [blue]{path.resolve()}[/blue]\n"
|
|
629
|
+
f"[bold sky_blue1]Output Dir:[/bold sky_blue1] [blue]{base_output_dir.resolve()}[/blue]\n"
|
|
630
|
+
f"[bold sky_blue1]Formats:[/bold sky_blue1] [white]{', '.join([f.upper() for f in formats_to_gen])}[/white]\n"
|
|
631
|
+
)
|
|
632
|
+
console.print(Panel(conf_content, title="[bold white]Report Configuration[/bold white]", border_style="bright_cyan", expand=False))
|
|
633
|
+
console.print()
|
|
634
|
+
|
|
635
|
+
try:
|
|
636
|
+
from codesecure.common.models import ScanResult
|
|
637
|
+
|
|
638
|
+
with open(path, "r", encoding="utf-8") as f:
|
|
639
|
+
data = json.load(f)
|
|
640
|
+
|
|
641
|
+
print_info(f"Loaded JSON payload from '{path.name}'...")
|
|
642
|
+
|
|
643
|
+
# Validating the JSON string data against the Pydantic ScanResult model
|
|
644
|
+
result = ScanResult.model_validate(data)
|
|
645
|
+
print_success(f"Successfully unpacked {result.total_findings} findings.")
|
|
646
|
+
|
|
647
|
+
except Exception as e:
|
|
648
|
+
print_error(f"Failed to load or parse JSON scan result: {e}")
|
|
649
|
+
sys.exit(1)
|
|
650
|
+
|
|
651
|
+
console.print(f"\n[bold white]š Generated Reports[/bold white]")
|
|
652
|
+
report_files = []
|
|
653
|
+
|
|
654
|
+
# Re-use the _generate_report logic passing the re-created result obj
|
|
655
|
+
for fmt in formats_to_gen:
|
|
656
|
+
try:
|
|
657
|
+
report_path = _generate_report(result, fmt, base_output_dir, path)
|
|
658
|
+
report_files.append((fmt.upper(), report_path.name))
|
|
659
|
+
except Exception as e:
|
|
660
|
+
print_error(f" ⢠{fmt.upper():<8} : Failed ({e})")
|
|
661
|
+
|
|
662
|
+
for fmt_name, filename in report_files:
|
|
663
|
+
console.print(f" ⢠[bold sky_blue1]{fmt_name:<10}:[/bold sky_blue1] [white]{filename}[/white]")
|
|
664
|
+
|
|
665
|
+
console.print("ā" * 50)
|
|
666
|
+
console.print(f"[bold white]š Finished:[/bold white] [bright_cyan]{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}[/bright_cyan]\n")
|
|
667
|
+
|
|
668
|
+
|
|
669
|
+
@cli.command("threat-model", hidden=True)
|
|
670
|
+
@click.argument("path", type=click.Path(exists=True, path_type=Path))
|
|
671
|
+
@click.option(
|
|
672
|
+
"--provider", "-p",
|
|
673
|
+
type=click.Choice(["google", "aws", "azure"]),
|
|
674
|
+
default="google",
|
|
675
|
+
help="AI provider for analysis"
|
|
676
|
+
)
|
|
677
|
+
@click.option(
|
|
678
|
+
"--output", "-o",
|
|
679
|
+
type=click.Path(path_type=Path),
|
|
680
|
+
default=None,
|
|
681
|
+
help="Output directory"
|
|
682
|
+
)
|
|
683
|
+
def threat_model(path: Path, provider: str, output: Optional[Path]):
|
|
684
|
+
"""
|
|
685
|
+
Generate STRIDE threat model.
|
|
686
|
+
|
|
687
|
+
PATH: Repository or architecture document path.
|
|
688
|
+
"""
|
|
689
|
+
print_info("Threat model generation using ThreatModel module")
|
|
690
|
+
print_warning("Threat model CLI integration pending TM-001 implementation")
|
|
691
|
+
|
|
692
|
+
|
|
693
|
+
@cli.command(hidden=True)
|
|
694
|
+
@click.argument("path", type=click.Path(exists=True, path_type=Path))
|
|
695
|
+
@click.option(
|
|
696
|
+
"--type", "-t",
|
|
697
|
+
type=click.Choice(["terraform", "cdk", "cloudformation"]),
|
|
698
|
+
default="terraform",
|
|
699
|
+
help="IaC type"
|
|
700
|
+
)
|
|
701
|
+
def validate(path: Path, type: str):
|
|
702
|
+
"""
|
|
703
|
+
Detect infrastructure drift.
|
|
704
|
+
|
|
705
|
+
PATH: IaC project path.
|
|
706
|
+
"""
|
|
707
|
+
print_info("Drift detection using SecurityValidate module")
|
|
708
|
+
print_warning("Validate CLI integration pending SV-001 implementation")
|
|
709
|
+
|
|
710
|
+
|
|
711
|
+
@cli.command(hidden=True)
|
|
712
|
+
@click.argument("path", type=click.Path(exists=True, path_type=Path))
|
|
713
|
+
def matrix(path: Path):
|
|
714
|
+
"""
|
|
715
|
+
Generate DSR Security Matrix via core framework.
|
|
716
|
+
|
|
717
|
+
PATH: Project path.
|
|
718
|
+
"""
|
|
719
|
+
print_info("Security Matrix generation using NeoLifter module")
|
|
720
|
+
print_warning("Matrix CLI integration pending SM-001 implementation")
|
|
721
|
+
|
|
722
|
+
|
|
723
|
+
@cli.command()
|
|
724
|
+
def list_scanners():
|
|
725
|
+
"""List available security scanners from Core."""
|
|
726
|
+
print_banner()
|
|
727
|
+
|
|
728
|
+
try:
|
|
729
|
+
local_scanners = _get_available_scanners(ScanMode.LOCAL)
|
|
730
|
+
all_scanners = _get_available_scanners(ScanMode.CONTAINER)
|
|
731
|
+
except Exception as e:
|
|
732
|
+
print_error(f"Failed to list scanners: {e}")
|
|
733
|
+
return
|
|
734
|
+
|
|
735
|
+
click.echo(click.style("\nš Available Scanners:\n", bold=True))
|
|
736
|
+
|
|
737
|
+
click.echo(click.style("Local + Container Mode:", fg="cyan", bold=True))
|
|
738
|
+
for name in local_scanners:
|
|
739
|
+
click.echo(f" ⢠{name}")
|
|
740
|
+
|
|
741
|
+
click.echo(click.style("\nContainer Mode Only:", fg="yellow", bold=True))
|
|
742
|
+
container_only = set(all_scanners) - set(local_scanners)
|
|
743
|
+
for name in container_only:
|
|
744
|
+
click.echo(f" ⢠{name}")
|
|
745
|
+
|
|
746
|
+
# Platform specific notes
|
|
747
|
+
if sys.platform == "win32":
|
|
748
|
+
click.echo()
|
|
749
|
+
print_warning("Note for Windows users:")
|
|
750
|
+
click.echo(" ⢠'semgrep' is not supported natively on Windows.")
|
|
751
|
+
click.echo(" ⢠Use WSL or Docker mode for full coverage.")
|
|
752
|
+
|
|
753
|
+
|
|
754
|
+
def main_entry():
|
|
755
|
+
"""Main entry point."""
|
|
756
|
+
# Register external commands
|
|
757
|
+
cli.add_command(feedback)
|
|
758
|
+
|
|
759
|
+
try:
|
|
760
|
+
cli()
|
|
761
|
+
except Exception as e:
|
|
762
|
+
# Let Click handle its own exceptions (Abort, UsageError, etc.)
|
|
763
|
+
if isinstance(e, click.ClickException) or isinstance(e, SystemExit):
|
|
764
|
+
raise
|
|
765
|
+
|
|
766
|
+
# Handle unexpected exceptions
|
|
767
|
+
console.print(f"\n[bold red]An unexpected error occurred: {e}[/bold red]")
|
|
768
|
+
|
|
769
|
+
# Ask for permission to send error report
|
|
770
|
+
if Confirm.ask("Would you like to send an automated error report to help us fix this?", default=True, console=console):
|
|
771
|
+
client = TelemetryClient()
|
|
772
|
+
with console.status("[bold green]Sending error report...", spinner="dots"):
|
|
773
|
+
tb = traceback.format_exc()
|
|
774
|
+
success = client.send_error_report(str(e), tb)
|
|
775
|
+
|
|
776
|
+
if success:
|
|
777
|
+
console.print("[green]Error report sent. Thank you![/green]")
|
|
778
|
+
else:
|
|
779
|
+
console.print("[yellow]Failed to send error report, but thank you for trying![/yellow]")
|
|
780
|
+
|
|
781
|
+
# Exit with error code
|
|
782
|
+
sys.exit(1)
|
|
783
|
+
|
|
784
|
+
|
|
785
|
+
if __name__ == "__main__":
|
|
786
|
+
main_entry()
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Setup utility to automatically configure CodeSecure PATH.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import sys
|
|
7
|
+
import platform
|
|
8
|
+
import subprocess
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def get_scripts_dir():
|
|
13
|
+
"""Detect where Python scripts are installed."""
|
|
14
|
+
if platform.system() == "Windows":
|
|
15
|
+
# Usually AppData\Roaming\Python\Python31x\Scripts for --user installs
|
|
16
|
+
# or the Python installation directory for global installs
|
|
17
|
+
import site
|
|
18
|
+
user_base = site.getuserbase()
|
|
19
|
+
if user_base:
|
|
20
|
+
py_version = f"Python{sys.version_info.major}{sys.version_info.minor}"
|
|
21
|
+
path = Path(user_base) / py_version / "Scripts"
|
|
22
|
+
if path.exists():
|
|
23
|
+
return path
|
|
24
|
+
|
|
25
|
+
# Fallback to sys.executable's directory
|
|
26
|
+
return Path(sys.executable).parent / "Scripts"
|
|
27
|
+
else:
|
|
28
|
+
# Linux/Mac
|
|
29
|
+
return Path.home() / ".local" / "bin"
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def setup_windows(scripts_path: Path):
|
|
33
|
+
"""Add path to Windows User Environment Variables."""
|
|
34
|
+
try:
|
|
35
|
+
scripts_dir = str(scripts_path.resolve())
|
|
36
|
+
# Use PowerShell to safely add to User PATH
|
|
37
|
+
cmd = f'[Environment]::SetEnvironmentVariable("Path", [Environment]::GetEnvironmentVariable("Path", "User") + ";{scripts_dir}", "User")'
|
|
38
|
+
subprocess.run(["powershell", "-Command", cmd], check=True)
|
|
39
|
+
return True
|
|
40
|
+
except Exception as e:
|
|
41
|
+
print(f"Error updating Windows PATH: {e}")
|
|
42
|
+
return False
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def setup_unix(scripts_path: Path):
|
|
46
|
+
"""Add path to shell profile for Linux/Mac."""
|
|
47
|
+
try:
|
|
48
|
+
scripts_dir = str(scripts_path.resolve())
|
|
49
|
+
shell = os.environ.get("SHELL", "").split("/")[-1]
|
|
50
|
+
|
|
51
|
+
if "zsh" in shell:
|
|
52
|
+
profile = Path.home() / ".zshrc"
|
|
53
|
+
elif "bash" in shell:
|
|
54
|
+
profile = Path.home() / ".bashrc"
|
|
55
|
+
else:
|
|
56
|
+
profile = Path.home() / ".profile"
|
|
57
|
+
|
|
58
|
+
line = f'\nexport PATH="$PATH:{scripts_dir}"\n'
|
|
59
|
+
|
|
60
|
+
with open(profile, "a") as f:
|
|
61
|
+
f.write(line)
|
|
62
|
+
return True, profile
|
|
63
|
+
except Exception as e:
|
|
64
|
+
print(f"Error updating Unix PATH: {e}")
|
|
65
|
+
return False, None
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def main():
|
|
69
|
+
print("š§ CodeSecure CLI Setup")
|
|
70
|
+
scripts_dir = get_scripts_dir()
|
|
71
|
+
|
|
72
|
+
if not scripts_dir.exists():
|
|
73
|
+
print(f"ā Could not find scripts directory at {scripts_dir}")
|
|
74
|
+
sys.exit(1)
|
|
75
|
+
|
|
76
|
+
print(f"š Detected scripts directory: {scripts_dir}")
|
|
77
|
+
|
|
78
|
+
confirm = input("Do you want to add this directory to your PATH? (y/n): ")
|
|
79
|
+
if confirm.lower() != 'y':
|
|
80
|
+
print("Setup cancelled.")
|
|
81
|
+
return
|
|
82
|
+
|
|
83
|
+
if platform.system() == "Windows":
|
|
84
|
+
if setup_windows(scripts_dir):
|
|
85
|
+
print("\nā
PATH updated successfully!")
|
|
86
|
+
print("š Please RESTART your terminal/PowerShell for changes to take effect.")
|
|
87
|
+
else:
|
|
88
|
+
success, profile = setup_unix(scripts_dir)
|
|
89
|
+
if success:
|
|
90
|
+
print(f"\nā
PATH added to {profile}")
|
|
91
|
+
print(f"š Run 'source {profile}' or restart your terminal.")
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
if __name__ == "__main__":
|
|
95
|
+
main()
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: codesecure-cli
|
|
3
|
+
Version: 1.0.0b10
|
|
4
|
+
Summary: CodeSecure Command Line Interface
|
|
5
|
+
License: MIT
|
|
6
|
+
Requires-Python: >=3.10
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
Requires-Dist: codesecure-core
|
|
9
|
+
Requires-Dist: click>=8.0.0
|
|
10
|
+
Requires-Dist: rich>=13.0.0
|
|
11
|
+
|
|
12
|
+
# CodeSecure CLI (`codesecure-cli`)
|
|
13
|
+
|
|
14
|
+
The `codesecure-cli` package is the user-facing terminal interface for the CodeSecure monorepo. It wraps the functionality of the core orchestration engine within an interactive, rich terminal application.
|
|
15
|
+
|
|
16
|
+
## šÆ Module Purpose
|
|
17
|
+
|
|
18
|
+
This package focuses entirely on **Developer Experience (DX)** inside the terminal and CI/CD environments. It parses CLI arguments using `Click`, dynamically structures interactive menus with `Rich`, and connects seamlessly to the Python business logic embedded in `codesecure-core`.
|
|
19
|
+
|
|
20
|
+
*Note: The CLI is designed to be a "Thin Wrapper". Core scanning logic does not exist in this package.*
|
|
21
|
+
|
|
22
|
+
## š¦ Local Installation
|
|
23
|
+
|
|
24
|
+
Installing the CLI relies on standard Python setups but requires the workspace dependency mapped to `codesecure-core`.
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
cd packages/cli
|
|
28
|
+
python -m venv .venv
|
|
29
|
+
source .venv/bin/activate
|
|
30
|
+
|
|
31
|
+
# Recommended: Install via pip from the project root rather than locally
|
|
32
|
+
# This dynamically resolves workspace dependencies
|
|
33
|
+
cd ../../
|
|
34
|
+
pip install -e ./packages/cli
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## š Exported Commands & Features
|
|
38
|
+
|
|
39
|
+
The entry point script installs a global binary command: `codesecure`. Check the top-level commands below.
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
# Validates the active scanners provided by Core
|
|
43
|
+
codesecure list-scanners
|
|
44
|
+
|
|
45
|
+
# Initializes Beta agreements
|
|
46
|
+
codesecure init
|
|
47
|
+
|
|
48
|
+
# Prompts setup for Gemini or Kiro CLI
|
|
49
|
+
codesecure login
|
|
50
|
+
|
|
51
|
+
# The main workhorse: Initiates an interactive async scan on a target path
|
|
52
|
+
codesecure scan ./my-project --provider kiro --output json,html
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## š ļø Integration Example
|
|
56
|
+
|
|
57
|
+
Since the CLI is a consumption edge-node in the monorepo architecture, its integrations primarily concern reading from `core`. Below is a snippet of how the CLI bypasses MCP overhead to list scanners efficiently from the programmatic core.
|
|
58
|
+
|
|
59
|
+
```python
|
|
60
|
+
import click
|
|
61
|
+
from codesecure.common.models import ScanMode
|
|
62
|
+
|
|
63
|
+
@click.command()
|
|
64
|
+
def list_scanners():
|
|
65
|
+
"""List available security scanners from Core."""
|
|
66
|
+
from codesecure.scanners.engine import get_scanner_engine
|
|
67
|
+
engine = get_scanner_engine()
|
|
68
|
+
|
|
69
|
+
try:
|
|
70
|
+
local_scanners = engine.get_available_scanners(ScanMode.LOCAL)
|
|
71
|
+
print(f"Available Local Scanners: {', '.join(local_scanners)}")
|
|
72
|
+
except Exception as e:
|
|
73
|
+
print(f"Failed to list scanners: {e}")
|
|
74
|
+
```
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
setup.py
|
|
4
|
+
src/codesecure_cli/__init__.py
|
|
5
|
+
src/codesecure_cli/feedback.py
|
|
6
|
+
src/codesecure_cli/main.py
|
|
7
|
+
src/codesecure_cli/setup.py
|
|
8
|
+
src/codesecure_cli.egg-info/PKG-INFO
|
|
9
|
+
src/codesecure_cli.egg-info/SOURCES.txt
|
|
10
|
+
src/codesecure_cli.egg-info/dependency_links.txt
|
|
11
|
+
src/codesecure_cli.egg-info/entry_points.txt
|
|
12
|
+
src/codesecure_cli.egg-info/requires.txt
|
|
13
|
+
src/codesecure_cli.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
codesecure_cli
|