Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions security-scanner/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
.env
tmp
63 changes: 63 additions & 0 deletions security-scanner/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
# Security Assistant

Security Assistant is a CLI that orchestrates multiple security steps for a project:
1) CycloneDX BOM generation
2) Trivy scan of the BOM
3) Dependency-Track upload and finding retrieval
4) Snyk scan (runs last so its output stays visible)

It can run interactively or in fully non-interactive (`--forced`) mode.

## Prerequisites
- Python 3 available on your PATH (invoked as `python3`).
- Docker + Docker Compose (for local Dependency-Track runs).
- `npm` available if Snyk needs to be installed automatically.
- Optional tokens:
- `DTRACK_API_KEY` (or stored via prompt) for Dependency-Track uploads/fetches.
- `SNYK_TOKEN` (optional); in forced mode Snyk will skip auth if no token is set.

## Quick start example
Forced, remote Dependency-Track server, Node 20.19.0:
```bash
python3 security_assistant.py \
-p /absolute/path/to/app/ \
--node-version 20.19.0 \
--dependency-track-server http://url:port/ \
--forced
```

## Usage
```bash
python3 security_assistant.py [options]
```

Key options:
- `-p, --path APP_ROOT` Project root.
- `--forced` Fully non-interactive mode.
- `--node-version VERSION` Node version to use via nvm for BOM generation when no nvm default is configured.
- `--dependency-track-server URL` Use an existing Dependency-Track API base URL (skips local stack).
- `--show-dtrack-findings` Fetch Dependency-Track findings even if the upload step is skipped (e.g., existing BOM).
- `--project-name NAME` / `--project-version VERSION` Override detected metadata.
- `--no-bom` / `--no-trivy` / `--no-dtrack` / `--no-snyk` Skip specific steps.
- `--dtrack-api-port` / `--dtrack-ui-port` Ports when starting local Dependency-Track.

Run `python3 security_assistant.py --help` for the full list.

## Behavior notes
- Project metadata: name/version are read from `package.json`. If missing and no overrides are provided, you will be prompted (or forced mode will fail unless you supply them).
- Dependency-Track:
- If the BOM upload returns HTTP 409, the assistant treats it as “already exists” and shows findings from the existing BOM.
- `--show-dtrack-findings` lets you view findings without uploading a BOM in this run.
- If you pass a URL pointing to the UI port (8080), it will automatically switch to the API port (8081).
- Snyk:
- Runs last so its output remains visible.
- In forced mode, if `SNYK_TOKEN` is absent it skips auth and attempts the scan (may fail if the CLI is not already authenticated).
- Summary: the final summary prints the Dependency-Track URL you provided (or localhost defaults) along with Snyk/Trivy notes.

## Typical flow
1) Generate `bom.json` (or reuse an existing one).
2) Scan the BOM with Trivy.
3) Upload BOM to Dependency-Track and fetch findings (or just fetch findings when requested).
4) Run `snyk test`.

Exit codes are non-zero in forced mode when a required step fails. Duplicate BOMs (HTTP 409) are treated as soft success so findings can still be displayed.
127 changes: 127 additions & 0 deletions security-scanner/bom_tool.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
from __future__ import annotations

from pathlib import Path
from typing import Optional

from utils import ConsoleUtils


class BomTool:
"""Generates a CycloneDX BOM (bom.json) for a Node-based project."""

def __init__(self, utils: ConsoleUtils) -> None:
self.u = utils
self.node_version: Optional[str] = None # Node version to use via nvm (optional)

def run(
self,
app_root: str,
node_version: Optional[str] = None,
interactive: bool = True,
) -> Optional[Path]:
"""
Orchestrates BOM generation:
- Ask (optional) Node.js version (via nvm)
- Run npm install + CycloneDX BOM generation
"""
self.u.print_header("Step 2 - CycloneDX BOM generation")

if interactive and node_version is None:
self._ask_node_version()
else:
self.node_version = node_version

return self._generate_bom(app_root)

# ------------------------------------------------------------------
# Node.js version (optional, via nvm)
# ------------------------------------------------------------------

def _ask_node_version(self) -> None:
self.u.print_step("Node.js version (optional)")

print(
"By default this step will use whatever 'node' is in your PATH.\n"
"If you know a specific Node.js version (managed by nvm) that should be used\n"
"for this project (for example 20.19.0), you can enter it here.\n"
"If you later see 'npm WARN EBADENGINE Unsupported engine', you can cancel\n"
"with Ctrl+C and rerun this assistant with another Node version.\n"
)

version = input("Node.js version to use with nvm (leave empty to use current): ").strip()
self.node_version = version or None
if self.node_version:
print(f"Will try to use Node.js {self.node_version} via nvm.")
else:
print("Using current Node.js from PATH (no nvm override).")

# ------------------------------------------------------------------
# BOM generation (single shell command)
# ------------------------------------------------------------------

def _generate_bom(self, app_root: str) -> Optional[Path]:
"""
Generate a CycloneDX BOM (bom.json) using npm + npx in a single shell command.
This will:
- Print node -v
- Run npm install
- Run npx @cyclonedx/cyclonedx-npm to produce bom.json
Optionally, it will switch Node.js version using nvm first.
"""

if not self.u.find_executable("npm"):
print("❌ npm is not available in PATH. Cannot generate BOM.")
return None

print(
"\nℹ️ If you see 'npm WARN EBADENGINE Unsupported engine', it means the project\n"
" expects a different Node.js version (for example ^20.19.0).\n"
" In that case, you can cancel with Ctrl+C and rerun this assistant with\n"
" a different Node version when prompted.\n"
)

self.u.print_step("Installing dependencies and generating CycloneDX BOM (npm install + npx)")

if self.node_version:
# Single shell command:
# - Load nvm (if available)
# - nvm install <version>
# - nvm use <version>
# - node -v
# - npm install
# - npx @cyclonedx/cyclonedx-npm ...
shell_cmd = (
"source ~/.nvm/nvm.sh 2>/dev/null || "
"source /usr/share/nvm/nvm.sh 2>/dev/null || "
"source /usr/local/opt/nvm/nvm.sh 2>/dev/null || true; "
f"nvm install {self.node_version}; "
f"nvm use {self.node_version}; "
"node -v; "
"npm install; "
"npx @cyclonedx/cyclonedx-npm "
"--output-file bom.json "
"--output-format json"
)
rc = self.u.run_command(["bash", "-lc", shell_cmd], cwd=app_root)
else:
# No nvm: use whatever Node.js is in PATH
shell_cmd = (
"node -v; "
"npm install; "
"npx @cyclonedx/cyclonedx-npm "
"--output-file bom.json "
"--output-format json"
)
rc = self.u.run_command(["bash", "-lc", shell_cmd], cwd=app_root)

if rc != 0:
print("❌ Failed to generate bom.json (npm install or npx failed).")
return None

bom_path = Path(app_root) / "bom.json"
if not bom_path.is_file():
print("❌ bom.json not found after generation.")
return None

print(f"✅ BOM generated at: {bom_path}")
return bom_path
Loading