Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
0fbdec2
Add artifact retention configuration options
ishitaajain22-tech Jun 9, 2026
45a8940
Update main.py
ishitaajain22-tech Jun 9, 2026
9d74b84
Update cli.py
ishitaajain22-tech Jun 9, 2026
4bdc4fe
Update .env.example
ishitaajain22-tech Jun 9, 2026
2bb49c8
Add files via upload
ishitaajain22-tech Jun 9, 2026
85f1a6d
Add files via upload
ishitaajain22-tech Jun 9, 2026
efd2645
Update main.py
ishitaajain22-tech Jun 9, 2026
29425fa
Update retention.py
ishitaajain22-tech Jun 9, 2026
24f43e1
Update test_retention.py
ishitaajain22-tech Jun 9, 2026
0010734
Update test_retention.py
ishitaajain22-tech Jun 9, 2026
2084cf9
Update test_retention.py
ishitaajain22-tech Jun 9, 2026
5bc85ad
Update retention.py
ishitaajain22-tech Jun 9, 2026
54d5c95
Fix missing newline at end of main.py
ishitaajain22-tech Jun 9, 2026
1d2d520
Ensure settings instance is created
ishitaajain22-tech Jun 9, 2026
3abe357
Update config.py
ishitaajain22-tech Jun 9, 2026
37da3ed
Update cli.py
ishitaajain22-tech Jun 9, 2026
8d40ccf
Update .env.example
ishitaajain22-tech Jun 9, 2026
752083a
Update retention.py
ishitaajain22-tech Jun 9, 2026
ee3fe85
Update test_retention.py
ishitaajain22-tech Jun 9, 2026
0290313
Update test_retention.py
ishitaajain22-tech Jun 9, 2026
51c0738
Update retention.py
ishitaajain22-tech Jun 9, 2026
18bd798
Update retention.py
ishitaajain22-tech Jun 9, 2026
e27bcce
Update test_retention.py
ishitaajain22-tech Jun 9, 2026
97493d8
Update .pre-commit-config.yaml
ishitaajain22-tech Jun 9, 2026
9446113
Ensure submodules setting remains false
ishitaajain22-tech Jun 9, 2026
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
22 changes: 12 additions & 10 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
```
# SecuScan Environment Configuration
# Copy this file to `.env` and adjust values for your local setup.

Expand All @@ -14,8 +15,6 @@ SECUSCAN_BIND_PORT=8000

# Docker Support
SECUSCAN_DOCKER_ENABLED=false
# Docker sandbox network (auto-created if absent; ICC disabled for isolation)
SECUSCAN_DOCKER_NETWORK=restricted

# Security Defaults
SECUSCAN_SAFE_MODE_DEFAULT=true
Expand All @@ -24,13 +23,6 @@ SECUSCAN_ALLOW_LOOPBACK_SCANS=true
# SECUSCAN_ALLOWED_NETWORKS=127.0.0.1,192.168.*.*,10.*.*.*,172.16.*.*
# SECUSCAN_CORS_ALLOWED_ORIGINS=http://127.0.0.1:5173,http://localhost:5173

# Network Policy & Admin Authentication
# SECUSCAN_NETWORK_ALLOWLIST=
# SECUSCAN_NETWORK_DENYLIST=169.254.169.254/32,127.0.0.0/8,10.0.0.0/8,172.16.0.0/12,192.168.0.0/16
# SECUSCAN_ENFORCE_NETWORK_POLICY=true
# SECUSCAN_NETWORK_POLICY_FAILURE_MODE=block
# SECUSCAN_ADMIN_API_KEY=replace-with-a-secure-admin-token

# Credential Vault — REQUIRED before first run
# Generate with: python -c "import secrets; print(secrets.token_hex(32))"
# The server refuses to start the vault if this is unset.
Expand All @@ -46,8 +38,9 @@ SECUSCAN_VAULT_KEY=replace-with-output-of-secrets.token_hex-32
# Supported values: network, filesystem, docker, credentials, intrusive, exploit
# Example: deny all exploitation and credential-accessing plugins:
# SECUSCAN_DENIED_CAPABILITIES=exploit,credentials

# Parser Sandbox Limits
# Plugin parser.py files run in isolated subprocesses. Adjust these if you have
# Plugin parser.py files run in isolated subprocesses. Adjust these if you have
# plugins that produce very large output or need more time to parse.
# SECUSCAN_PARSER_SANDBOX_TIMEOUT_SECONDS=30
# SECUSCAN_PARSER_SANDBOX_MAX_OUTPUT_BYTES=8388608
Expand All @@ -56,3 +49,12 @@ SECUSCAN_VAULT_KEY=replace-with-output-of-secrets.token_hex-32
# Leave these unset for the default local dev flow.
# VITE_API_PROXY_TARGET=http://127.0.0.1:8000
# VITE_API_BASE=http://127.0.0.1:8000/api/v1

# Artifact Retention (optional)
# max_age_days=0 / max_task_count=0 disables that policy.
# The background loop runs every interval_seconds (default: 3600 = 1 hour).
# SECUSCAN_RETENTION_MAX_AGE_DAYS=90
# SECUSCAN_RETENTION_MAX_TASK_COUNT=500
# SECUSCAN_RETENTION_KEEP_STATUSES=running,queued
# SECUSCAN_RETENTION_INTERVAL_SECONDS=3600
```
9 changes: 9 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,12 @@ repos:
hooks:
- id: black
language_version: python3

ci:
autofix_commit_msg: 'fix: pre-commit auto-fixes'
autofix_prs: true
autoupdate_branch: ''
autoupdate_commit_msg: 'chore: pre-commit autoupdate'
autoupdate_schedule: weekly
skip: []
submodules: false
73 changes: 73 additions & 0 deletions backend/secuscan/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,44 @@ async def monitor_output():

return 0

async def run_retention_cleanup(
max_age_days: int,
max_task_count: int,
keep_statuses: str,
dry_run: bool,
) -> int:
"""Perform a one-shot retention cleanup run and print a summary."""
settings.ensure_directories()
await init_db(settings.database_path)

from backend.secuscan.database import get_db
from backend.secuscan.retention import run_cleanup

db = await get_db()
keep_set = {s.strip() for s in keep_statuses.split(",") if s.strip()}

result = await run_cleanup(
db,
max_age_days=max_age_days,
max_task_count=max_task_count,
keep_statuses=keep_set,
dry_run=dry_run,
)

label = "[DRY-RUN] " if dry_run else ""
print(f"{label}Tasks {'would be ' if dry_run else ''}removed: {result.task_count}")
print(f"{label}Files {'would be ' if dry_run else ''}removed: {result.file_count}")
if result.tasks_removed:
for tid in result.tasks_removed:
print(f" {'would remove' if dry_run else 'removed'}: {tid}")
if result.errors:
print(f"Errors ({len(result.errors)}):")
for err in result.errors:
print(f" {err}")
return 1
return 0


def main():
parser = argparse.ArgumentParser(description="SecuScan CLI - Local-First Pentesting Toolkit")
subparsers = parser.add_subparsers(dest="command", help="Command to run")
Expand All @@ -147,10 +185,45 @@ def main():
# List plugins command
subparsers.add_parser("plugins", help="List available plugins")

# Cleanup command
cleanup_parser = subparsers.add_parser(
"cleanup",
help="Run artifact retention cleanup (supports --dry-run)",
)
cleanup_parser.add_argument(
"--max-age-days",
type=int,
default=settings.retention_max_age_days,
help="Remove tasks older than N days (0 = disabled)",
)
cleanup_parser.add_argument(
"--max-task-count",
type=int,
default=settings.retention_max_task_count,
help="Keep only the N most-recent tasks (0 = disabled)",
)
cleanup_parser.add_argument(
"--keep-statuses",
default=settings.retention_keep_statuses,
help="Comma-separated list of statuses to never purge (default: running,queued)",
)
cleanup_parser.add_argument(
"--dry-run",
action="store_true",
help="Print what would be deleted without making any changes",
)

args = parser.parse_args()

if args.command == "scan":
sys.exit(asyncio.run(run_scan(args.target, args.plugin, args.format, args.output)))
elif args.command == "cleanup":
sys.exit(asyncio.run(run_retention_cleanup(
max_age_days=args.max_age_days,
max_task_count=args.max_task_count,
keep_statuses=args.keep_statuses,
dry_run=args.dry_run,
)))
elif args.command == "plugins":
# Synchronous shortcut for listing
async def list_plugins():
Expand Down
14 changes: 14 additions & 0 deletions backend/secuscan/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,15 @@ class Settings(BaseSettings):
parser_sandbox_timeout_seconds: int = 30
parser_sandbox_max_output_bytes: int = 8 * 1024 * 1024 # 8 MB

# Artifact Retention
# max_age_days=0 disables age-based cleanup; max_task_count=0 disables count-based cleanup.
retention_max_age_days: int = 0
retention_max_task_count: int = 0
# Comma-separated statuses that are never automatically purged.
retention_keep_statuses: str = "running,queued"
# How often (seconds) the background retention loop runs.
retention_interval_seconds: int = 3600

# Logging
log_level: str = "INFO"
log_file: str = str(PROJECT_ROOT / "logs" / "secuscan.log")
Expand Down Expand Up @@ -152,6 +161,11 @@ def base_url(self) -> str:
"""Full base URL for the API"""
return f"http://{self.bind_address}:{self.bind_port}"

@property
def retention_keep_statuses_set(self) -> set:
"""Return retention_keep_statuses as a Python set for easy membership tests."""
return {s.strip() for s in self.retention_keep_statuses.split(",") if s.strip()}

@property
def resolved_vault_key(self) -> bytes:
"""Return a deterministic 32-byte key for credential vault encryption.
Expand Down
29 changes: 20 additions & 9 deletions backend/secuscan/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
from .routes import router
from .saved_views import saved_views_router
from .workflows import scheduler
from .retention import retention_scheduler

logging.basicConfig(
level=getattr(logging, settings.log_level),
Expand All @@ -45,22 +46,22 @@ async def lifespan(app: FastAPI):
"""Application lifespan manager"""
# Startup
logger.info("🚀 Starting SecuScan backend...")

# Ensure directories exist
settings.ensure_directories()
logger.info("✓ Directories initialized")

# Initialize API key authentication
api_key = init_api_key(settings.data_dir)
logger.info("✓ API key authentication ready (key file: %s/.api_key)", settings.data_dir)

# Initialize database
await init_db(settings.database_path)
logger.info("✓ SQLite connected")

await init_cache()
logger.info("✓ In-memory cache initialized")

# Load plugins
await init_plugins(settings.plugins_dir)
logger.info("✓ Plugins loaded")
Expand Down Expand Up @@ -107,18 +108,28 @@ async def lifespan(app: FastAPI):

await scheduler.start()
logger.info("✓ Workflow scheduler started")


# Start artifact retention background loop (no-op when all limits are 0)
await retention_scheduler.start(
interval_seconds=settings.retention_interval_seconds,
max_age_days=settings.retention_max_age_days,
max_task_count=settings.retention_max_task_count,
keep_statuses=settings.retention_keep_statuses_set,
)
logger.info("✓ Retention scheduler started")

logger.info("✓ Ready to serve on %s:%d", settings.bind_address, settings.bind_port)

yield

# Shutdown
logger.info("🛑 Shutting down SecuScan backend...")
if global_db:
await global_db.disconnect()
if global_cache:
await global_cache.disconnect()
await scheduler.stop()
await retention_scheduler.stop()
logger.info("✓ Shutdown complete")

# Create FastAPI application
Expand Down Expand Up @@ -175,7 +186,7 @@ async def health_check():
"""Health check endpoint"""
import platform
import sys

return {
"status": "operational",
"version": "0.1.0-alpha",
Expand All @@ -201,7 +212,7 @@ async def root():
def main():
"""Main entry point"""
import uvicorn

logger.info("""
╔═══════════════════════════════════════════════════════╗
║ ║
Expand All @@ -212,7 +223,7 @@ def main():
║ ║
╚═══════════════════════════════════════════════════════╝
""")

uvicorn.run(
"backend.secuscan.main:app",
host=settings.bind_address,
Expand Down
Loading
Loading