scanner

package
v0.24.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Apr 11, 2026 License: MIT Imports: 15 Imported by: 0

Documentation

Index

Constants

View Source
const (
	DefaultWorkerPoolSize = 3  // Max concurrent scans
	MaxQueueSize          = 50 // Max pending scans in queue
)
View Source
const (
	QueueStatusPending   = "pending"
	QueueStatusRunning   = "running"
	QueueStatusCompleted = "completed"
	QueueStatusFailed    = "failed"
	QueueStatusSkipped   = "skipped"
	QueueStatusCancelled = "cancelled"
)

QueueItemStatus represents the status of a queued scan

View Source
const (
	// ScannerStatusAvailable means the scanner is known to mcpproxy but
	// not enabled. Toggling it on moves it into ScannerStatusPulling.
	ScannerStatusAvailable = "available"
	// ScannerStatusPulling means the Docker image is currently being
	// downloaded in the background. UI should show a spinner.
	ScannerStatusPulling = "pulling"
	// ScannerStatusInstalled means the image is present locally and the
	// scanner is ready to run. No required API keys are configured yet.
	ScannerStatusInstalled = "installed"
	// ScannerStatusConfigured means the image is present AND user-supplied
	// env (e.g. API keys) have been stored.
	ScannerStatusConfigured = "configured"
	// ScannerStatusError means the last operation (typically a pull) failed.
	// ErrorMsg carries the reason. The UI should offer a "Retry" button.
	ScannerStatusError = "error"
)

Scanner status constants

View Source
const (
	ScanJobStatusPending   = "pending"
	ScanJobStatusRunning   = "running"
	ScanJobStatusCompleted = "completed"
	ScanJobStatusFailed    = "failed"
	ScanJobStatusCancelled = "cancelled"
)

Scan job status constants

View Source
const (
	SeverityCritical = "critical"
	SeverityHigh     = "high"
	SeverityMedium   = "medium"
	SeverityLow      = "low"
	SeverityInfo     = "info"
)

Scan finding severity constants

View Source
const (
	ScanPassSecurityScan     = 1 // Fast pass: source code + lockfile scanning
	ScanPassSupplyChainAudit = 2 // Background pass: full filesystem CVE analysis
)

Scan pass constants

View Source
const (
	MaxScanLogLines   = 5000   // Max lines of stdout/stderr per scanner
	MaxScannedFiles   = 10000  // Max file entries in scanned_files list
	MaxScansPerServer = 20     // Keep last N scans per server
	MaxLogBytes       = 500000 // ~500KB max per log field
)

Scan context limits

View Source
const (
	ThreatToolPoisoning   = "tool_poisoning"   // Hidden instructions in tool descriptions
	ThreatPromptInjection = "prompt_injection" // Malicious payloads in tool responses
	ThreatRugPull         = "rug_pull"         // Tool definitions changed after approval
	ThreatSupplyChain     = "supply_chain"     // Known CVEs in dependencies
	ThreatMaliciousCode   = "malicious_code"   // Malware, backdoors, suspicious code
	ThreatUncategorized   = "uncategorized"    // Other findings
)

User-facing threat category constants

View Source
const (
	ThreatLevelDangerous = "dangerous" // Blocks approval: tool poisoning, active injection
	ThreatLevelWarning   = "warning"   // Rug pull, high CVEs
	ThreatLevelInfo      = "info"      // Low CVEs, informational
)

User-facing severity levels (simpler than CVSS)

Variables

This section is empty.

Functions

func CalculateRiskScore

func CalculateRiskScore(findings []ScanFinding) int

CalculateRiskScore computes a 0-100 risk score from findings. Scoring is based on user-facing threat levels, not raw CVSS.

This uses logarithmic diminishing returns so duplicate findings from multiple scanners don't inflate the score, while still reflecting cumulative risk. Findings are deduplicated by (rule_id + location) before scoring.

Formula per category: category_score = weight * log2(1 + unique_count)

  • Dangerous: weight 25 (1 finding=25, 2=40, 4=58, 8=72, cap 80)
  • Warning: weight 6 (1 finding=6, 2=10, 4=15, 8=18, cap 25)
  • Info: weight 2 (1 finding=2, 2=3, 4=5, 8=6, cap 10)

Note: This score is an experimental heuristic. There is no industry standard for aggregating multi-scanner MCP security findings into a single number.

func ClassifyAllFindings

func ClassifyAllFindings(findings []ScanFinding)

ClassifyAllFindings applies threat classification to all findings

func ClassifyThreat

func ClassifyThreat(f *ScanFinding)

ClassifyThreat assigns user-facing threat_type and threat_level to a finding based on rule ID, category, description, and severity.

func CollectFileList

func CollectFileList(dir string) (files []string, totalFiles int, totalSize int64)

CollectFileList walks a directory and returns a list of files (relative paths). Caps at MaxScannedFiles entries. Also returns total count and size.

func GenerateContainerName

func GenerateContainerName(scannerID, serverName string) string

GenerateContainerName creates a unique container name for a scanner run

func IsSARIF

func IsSARIF(data []byte) bool

IsSARIF checks if the given data looks like a SARIF document

func PrepareReportDir

func PrepareReportDir(baseDir, jobID, scannerID string) (string, error)

PrepareReportDir creates a temporary directory for scanner output

func ValidateManifest

func ValidateManifest(s *ScannerPlugin) error

ValidateManifest validates a scanner plugin manifest

Types

type AggregatedReport

type AggregatedReport struct {
	JobID          string        `json:"job_id"`
	ServerName     string        `json:"server_name"`
	Findings       []ScanFinding `json:"findings"`
	RiskScore      int           `json:"risk_score"`
	Summary        ReportSummary `json:"summary"`
	ScannedAt      time.Time     `json:"scanned_at"`
	Reports        []ScanReport  `json:"reports"`
	ScannersRun    int           `json:"scanners_run"`    // How many scanners actually produced results
	ScannersFailed int           `json:"scanners_failed"` // How many scanners failed
	ScannersTotal  int           `json:"scanners_total"`  // Total scanners attempted
	ScanComplete   bool          `json:"scan_complete"`   // True only if at least one scanner succeeded
	EmptyScan      bool          `json:"empty_scan"`      // True when scanners ran but had no files to analyze
	// Two-pass scan tracking
	Pass1Complete bool `json:"pass1_complete"` // Security scan (fast) done
	Pass2Complete bool `json:"pass2_complete"` // Supply chain audit done
	Pass2Running  bool `json:"pass2_running"`  // Supply chain audit in progress
	// Scan context from the primary job (for report page display)
	ScanContext     *ScanContext       `json:"scan_context,omitempty"`
	ScannerStatuses []ScannerJobStatus `json:"scanner_statuses,omitempty"` // Per-scanner execution logs
}

AggregatedReport combines results from all scanners for a single scan job

func AggregateReports

func AggregateReports(jobID, serverName string, reports []*ScanReport) *AggregatedReport

AggregateReports combines multiple scan reports into an aggregated report. Note: scannersTotal and scannersFailed should be provided by the caller from the ScanJob.ScannerStatuses, since reports only contains successful results.

func AggregateReportsWithJobStatus

func AggregateReportsWithJobStatus(jobID, serverName string, reports []*ScanReport, job *ScanJob) *AggregatedReport

AggregateReportsWithJobStatus combines reports and enriches with scanner failure info from the job.

type DockerRunner

type DockerRunner struct {
	// contains filtered or unexported fields
}

DockerRunner executes Docker operations for scanner containers

func NewDockerRunner

func NewDockerRunner(logger *zap.Logger) *DockerRunner

NewDockerRunner creates a new DockerRunner

func (*DockerRunner) GetImageDigest

func (d *DockerRunner) GetImageDigest(ctx context.Context, image string) (string, error)

GetImageDigest returns the Docker image digest

func (*DockerRunner) ImageExists

func (d *DockerRunner) ImageExists(ctx context.Context, image string) bool

ImageExists checks if a Docker image exists locally

func (*DockerRunner) IsDockerAvailable

func (d *DockerRunner) IsDockerAvailable(ctx context.Context) bool

IsDockerAvailable checks if Docker daemon is running

func (*DockerRunner) KillContainer

func (d *DockerRunner) KillContainer(ctx context.Context, name string) error

KillContainer forcefully kills a running container

func (*DockerRunner) PullImage

func (d *DockerRunner) PullImage(ctx context.Context, image string) error

PullImage pulls a Docker image with progress logging

func (*DockerRunner) ReadReportFile

func (d *DockerRunner) ReadReportFile(reportDir string) ([]byte, error)

ReadReportFile reads the SARIF report from the report directory

func (*DockerRunner) RemoveImage

func (d *DockerRunner) RemoveImage(ctx context.Context, image string) error

RemoveImage removes a Docker image

func (*DockerRunner) RunScanner

func (d *DockerRunner) RunScanner(ctx context.Context, cfg ScannerRunConfig) (stdout, stderr string, exitCode int, err error)

RunScanner runs a scanner container and returns the exit code and stdout/stderr

func (*DockerRunner) StopContainer

func (d *DockerRunner) StopContainer(ctx context.Context, name string, timeout int) error

StopContainer gracefully stops a container with timeout

type Engine

type Engine struct {
	// contains filtered or unexported fields
}

Engine orchestrates parallel scanner execution for a server

func NewEngine

func NewEngine(docker *DockerRunner, registry *Registry, dataDir string, logger *zap.Logger) *Engine

NewEngine creates a new scan orchestration engine

func (*Engine) CancelScan

func (e *Engine) CancelScan(serverName string) error

CancelScan cancels a running scan for a server

func (*Engine) GetActiveJob

func (e *Engine) GetActiveJob(serverName string) *ScanJob

GetActiveJob returns the active scan job for a server, if any

func (*Engine) StartScan

func (e *Engine) StartScan(ctx context.Context, req ScanRequest, callback ScanCallback) (*ScanJob, error)

StartScan begins a scan of the specified server Returns the scan job immediately; scanning runs in the background

type EnvRequirement

type EnvRequirement struct {
	Key    string `json:"key"`
	Label  string `json:"label"`
	Secret bool   `json:"secret"`
}

EnvRequirement represents a required or optional environment variable for a scanner

type EventEmitter

type EventEmitter interface {
	EmitSecurityScanStarted(serverName string, scanners []string, jobID string)
	EmitSecurityScanProgress(serverName, scannerID, status string, progress int)
	EmitSecurityScanCompleted(serverName string, findingsSummary map[string]int)
	EmitSecurityScanFailed(serverName, scannerID, errMsg string)
	EmitSecurityIntegrityAlert(serverName, alertType, action string)
	// EmitSecurityScannerChanged signals that a scanner plugin's state has
	// changed (e.g., background pull started/finished/failed) so the web UI
	// can refresh its scanner list without polling.
	EmitSecurityScannerChanged(scannerID, status, errMsg string)
}

EventEmitter defines how the service emits events

type FindingCounts

type FindingCounts struct {
	Dangerous int `json:"dangerous"`
	Warning   int `json:"warning"`
	Info      int `json:"info"`
	Total     int `json:"total"`
}

FindingCounts groups findings by user-facing threat level.

type IntegrityBaseline

type IntegrityBaseline struct {
	ServerName    string            `json:"server_name"`
	ImageDigest   string            `json:"image_digest"`
	SourceHash    string            `json:"source_hash"`
	LockfileHash  string            `json:"lockfile_hash"`
	DiffManifest  []string          `json:"diff_manifest,omitempty"`
	ToolHashes    map[string]string `json:"tool_hashes,omitempty"`
	ScanReportIDs []string          `json:"scan_report_ids,omitempty"`
	ApprovedAt    time.Time         `json:"approved_at"`
	ApprovedBy    string            `json:"approved_by"`
}

IntegrityBaseline represents the approved integrity state for a server

func (*IntegrityBaseline) MarshalBinary

func (b *IntegrityBaseline) MarshalBinary() ([]byte, error)

MarshalBinary implements encoding.BinaryMarshaler

func (*IntegrityBaseline) UnmarshalBinary

func (b *IntegrityBaseline) UnmarshalBinary(data []byte) error

UnmarshalBinary implements encoding.BinaryUnmarshaler

type IntegrityCheckResult

type IntegrityCheckResult struct {
	ServerName string               `json:"server_name"`
	Passed     bool                 `json:"passed"`
	CheckedAt  time.Time            `json:"checked_at"`
	Violations []IntegrityViolation `json:"violations,omitempty"`
}

IntegrityCheckResult holds the result of an integrity check

type IntegrityViolation

type IntegrityViolation struct {
	Type     string `json:"type"`
	Message  string `json:"message"`
	Expected string `json:"expected,omitempty"`
	Actual   string `json:"actual,omitempty"`
}

IntegrityViolation describes a specific integrity check failure

type NoopCallback

type NoopCallback struct{}

NoopCallback is a no-op implementation of ScanCallback

func (*NoopCallback) OnScanCompleted

func (n *NoopCallback) OnScanCompleted(_ *ScanJob, _ []*ScanReport)

func (*NoopCallback) OnScanFailed

func (n *NoopCallback) OnScanFailed(_ *ScanJob, _ error)

func (*NoopCallback) OnScanStarted

func (n *NoopCallback) OnScanStarted(_ *ScanJob)

func (*NoopCallback) OnScannerCompleted

func (n *NoopCallback) OnScannerCompleted(_ *ScanJob, _ string, _ *ScanReport)

func (*NoopCallback) OnScannerFailed

func (n *NoopCallback) OnScannerFailed(_ *ScanJob, _ string, _ error)

func (*NoopCallback) OnScannerStarted

func (n *NoopCallback) OnScannerStarted(_ *ScanJob, _ string)

type NoopEmitter

type NoopEmitter struct{}

NoopEmitter is a no-op implementation of EventEmitter

func (*NoopEmitter) EmitSecurityIntegrityAlert

func (n *NoopEmitter) EmitSecurityIntegrityAlert(string, string, string)

func (*NoopEmitter) EmitSecurityScanCompleted

func (n *NoopEmitter) EmitSecurityScanCompleted(string, map[string]int)

func (*NoopEmitter) EmitSecurityScanFailed

func (n *NoopEmitter) EmitSecurityScanFailed(string, string, string)

func (*NoopEmitter) EmitSecurityScanProgress

func (n *NoopEmitter) EmitSecurityScanProgress(string, string, string, int)

func (*NoopEmitter) EmitSecurityScanStarted

func (n *NoopEmitter) EmitSecurityScanStarted(string, []string, string)

func (*NoopEmitter) EmitSecurityScannerChanged

func (n *NoopEmitter) EmitSecurityScannerChanged(string, string, string)

type QueueItem

type QueueItem struct {
	ServerName string    `json:"server_name"`
	Status     string    `json:"status"`
	JobID      string    `json:"job_id,omitempty"`
	Error      string    `json:"error,omitempty"`
	StartedAt  time.Time `json:"started_at,omitempty"`
	DoneAt     time.Time `json:"done_at,omitempty"`
	SkipReason string    `json:"skip_reason,omitempty"` // Why the scan was skipped
}

QueueItem represents a single scan request in the queue

type QueueProgress

type QueueProgress struct {
	BatchID   string      `json:"batch_id"`
	Status    string      `json:"status"` // running, completed, cancelled
	Total     int         `json:"total"`
	Pending   int         `json:"pending"`
	Running   int         `json:"running"`
	Completed int         `json:"completed"`
	Failed    int         `json:"failed"`
	Skipped   int         `json:"skipped"`
	Cancelled int         `json:"cancelled"`
	StartedAt time.Time   `json:"started_at"`
	DoneAt    time.Time   `json:"done_at,omitempty"`
	Items     []QueueItem `json:"items"`
}

QueueProgress tracks overall scan-all progress

type Registry

type Registry struct {
	// contains filtered or unexported fields
}

Registry manages the scanner plugin registry

func NewRegistry

func NewRegistry(dataDir string, logger *zap.Logger) *Registry

NewRegistry creates a new scanner registry

func (*Registry) Get

func (r *Registry) Get(id string) (*ScannerPlugin, error)

Get returns a scanner by ID

func (*Registry) List

func (r *Registry) List() []*ScannerPlugin

List returns all known scanners (bundled + user) sorted by ID so that API consumers, CLI output, and the web UI all see a deterministic order.

func (*Registry) Register

func (r *Registry) Register(s *ScannerPlugin) error

Register adds a custom scanner to the registry It validates the manifest and saves to user registry file

func (*Registry) Unregister

func (r *Registry) Unregister(id string) error

Unregister removes a custom scanner from the registry Cannot unregister bundled scanners

func (*Registry) UpdateStatus

func (r *Registry) UpdateStatus(id, status string) error

UpdateStatus updates the status of a scanner in the registry

type ReportSummary

type ReportSummary struct {
	Critical  int `json:"critical"`
	High      int `json:"high"`
	Medium    int `json:"medium"`
	Low       int `json:"low"`
	Info      int `json:"info"`
	Total     int `json:"total"`
	Dangerous int `json:"dangerous"`  // Threat level: tool poisoning, prompt injection
	Warnings  int `json:"warnings"`   // Threat level: rug pull, high CVEs
	InfoLevel int `json:"info_level"` // Threat level: low CVEs, informational
}

ReportSummary provides counts by severity and threat level

func SummarizeFindings

func SummarizeFindings(findings []ScanFinding) ReportSummary

SummarizeFindings produces a ReportSummary from findings

type ResolvedSource

type ResolvedSource struct {
	SourceDir   string   // Host directory containing source files
	ContainerID string   // Docker container ID (if applicable)
	ServerURL   string   // URL for mcp_connection input (HTTP/SSE servers)
	Method      string   // How source was resolved: "docker_extract", "working_dir", "local_path", "url", "manual"
	Cleanup     func()   // Cleanup function (removes temp dirs)
	Files       []string // List of files found in source dir (capped)
	TotalFiles  int      // Total file count
	TotalSize   int64    // Total size in bytes
}

ResolvedSource contains the resolved source information for scanning

type SARIFArtifactLocation

type SARIFArtifactLocation struct {
	URI string `json:"uri"`
}

SARIFArtifactLocation describes a file path

type SARIFConfiguration

type SARIFConfiguration struct {
	Level string `json:"level,omitempty"`
}

SARIFConfiguration holds rule configuration

type SARIFDriver

type SARIFDriver struct {
	Name    string      `json:"name"`
	Version string      `json:"version,omitempty"`
	Rules   []SARIFRule `json:"rules,omitempty"`
}

SARIFDriver describes the scanner driver

type SARIFLocation

type SARIFLocation struct {
	PhysicalLocation *SARIFPhysicalLocation `json:"physicalLocation,omitempty"`
}

SARIFLocation describes where a finding was detected

type SARIFMessage

type SARIFMessage struct {
	Text string `json:"text"`
}

SARIFMessage holds text content

type SARIFPhysicalLocation

type SARIFPhysicalLocation struct {
	ArtifactLocation *SARIFArtifactLocation `json:"artifactLocation,omitempty"`
	Region           *SARIFRegion           `json:"region,omitempty"`
}

SARIFPhysicalLocation describes the physical file location

type SARIFRegion

type SARIFRegion struct {
	StartLine   int `json:"startLine,omitempty"`
	StartColumn int `json:"startColumn,omitempty"`
	EndLine     int `json:"endLine,omitempty"`
	EndColumn   int `json:"endColumn,omitempty"`
}

SARIFRegion describes a region within a file

type SARIFReport

type SARIFReport struct {
	Version string     `json:"version"`
	Schema  string     `json:"$schema,omitempty"`
	Runs    []SARIFRun `json:"runs"`
}

SARIFReport represents the top-level SARIF 2.1.0 document

func ParseSARIF

func ParseSARIF(data []byte) (*SARIFReport, error)

ParseSARIF parses a SARIF 2.1.0 JSON document

type SARIFResult

type SARIFResult struct {
	RuleID     string          `json:"ruleId,omitempty"`
	Level      string          `json:"level,omitempty"` // "error", "warning", "note", "none"
	Message    SARIFMessage    `json:"message"`
	Locations  []SARIFLocation `json:"locations,omitempty"`
	Properties map[string]any  `json:"properties,omitempty"`
}

SARIFResult represents an individual finding

type SARIFRule

type SARIFRule struct {
	ID               string              `json:"id"`
	ShortDescription *SARIFMessage       `json:"shortDescription,omitempty"`
	FullDescription  *SARIFMessage       `json:"fullDescription,omitempty"`
	DefaultConfig    *SARIFConfiguration `json:"defaultConfiguration,omitempty"`
	HelpURI          string              `json:"helpUri,omitempty"`
	Properties       map[string]any      `json:"properties,omitempty"`
}

SARIFRule defines a detection rule

type SARIFRun

type SARIFRun struct {
	Tool    SARIFTool     `json:"tool"`
	Results []SARIFResult `json:"results"`
}

SARIFRun represents a single scanner execution run

type SARIFTool

type SARIFTool struct {
	Driver SARIFDriver `json:"driver"`
}

SARIFTool describes the scanner that produced results

type ScanAllRequest

type ScanAllRequest struct {
	ScannerIDs  []string // Specific scanners (empty = all installed)
	SkipEnabled bool     // If true, only scan quarantined servers
}

ScanAllRequest defines what to scan

type ScanCallback

type ScanCallback interface {
	OnScanStarted(job *ScanJob)
	OnScannerStarted(job *ScanJob, scannerID string)
	OnScannerCompleted(job *ScanJob, scannerID string, report *ScanReport)
	OnScannerFailed(job *ScanJob, scannerID string, err error)
	OnScanCompleted(job *ScanJob, reports []*ScanReport)
	OnScanFailed(job *ScanJob, err error)
}

ScanCallback receives scan lifecycle events

type ScanContext

type ScanContext struct {
	SourceMethod    string   `json:"source_method"`             // "docker_extract", "working_dir", "local_path", "url", "none"
	SourcePath      string   `json:"source_path"`               // Actual path/URL that was scanned
	DockerIsolation bool     `json:"docker_isolation"`          // Whether server runs in Docker
	ContainerID     string   `json:"container_id,omitempty"`    // Docker container ID (if applicable)
	ContainerImage  string   `json:"container_image,omitempty"` // Docker image used
	ServerProtocol  string   `json:"server_protocol"`           // stdio, http, sse
	ServerCommand   string   `json:"server_command,omitempty"`  // Command used to start server
	ToolsExported   int      `json:"tools_exported,omitempty"`  // Number of tool definitions exported for scanning
	ScannedFiles    []string `json:"scanned_files,omitempty"`   // List of files that were scanned (capped at MaxScannedFiles)
	TotalFiles      int      `json:"total_files"`               // Total file count (may be > len(ScannedFiles) if capped)
	TotalSizeBytes  int64    `json:"total_size_bytes"`          // Total size of scanned source
}

ScanContext describes what was scanned and how the source was resolved. This gives users full transparency into what the scanners actually checked.

type ScanFinding

type ScanFinding struct {
	RuleID           string  `json:"rule_id"`
	Severity         string  `json:"severity"`     // critical, high, medium, low, info
	Category         string  `json:"category"`     // SARIF category
	ThreatType       string  `json:"threat_type"`  // User-facing: tool_poisoning, prompt_injection, rug_pull, supply_chain
	ThreatLevel      string  `json:"threat_level"` // User-facing: dangerous, warning, info
	Title            string  `json:"title"`
	Description      string  `json:"description"`
	Location         string  `json:"location,omitempty"`
	Scanner          string  `json:"scanner"`
	HelpURI          string  `json:"help_uri,omitempty"`          // Link to CVE/advisory details
	CVSSScore        float64 `json:"cvss_score,omitempty"`        // CVSS severity score (0-10)
	PackageName      string  `json:"package_name,omitempty"`      // Affected package
	InstalledVersion string  `json:"installed_version,omitempty"` // Current version
	FixedVersion     string  `json:"fixed_version,omitempty"`     // Version with fix
	ScanPass         int     `json:"scan_pass,omitempty"`         // 1 = security scan, 2 = supply chain audit
	Evidence         string  `json:"evidence,omitempty"`          // The text/content that triggered the finding
	// SupplyChainAudit marks findings that belong in the "Supply Chain Audit (CVEs)"
	// UI section regardless of which pass produced them. True only for real CVE/package
	// vulnerabilities (CVE-prefixed rule ID or a populated PackageName). AI scanner and
	// other non-package findings stay false so the UI can route them to their proper
	// threat_type group instead of the CVE section.
	SupplyChainAudit bool `json:"supply_chain_audit,omitempty"`
}

ScanFinding represents an individual security finding

func NormalizeFindings

func NormalizeFindings(report *SARIFReport, scannerID string) []ScanFinding

NormalizeFindings converts SARIF results into normalized ScanFindings

type ScanJob

type ScanJob struct {
	ID          string    `json:"id"`
	ServerName  string    `json:"server_name"`
	Status      string    `json:"status"`    // pending, running, completed, failed, cancelled
	ScanPass    int       `json:"scan_pass"` // 1 = security scan (fast), 2 = supply chain audit (background)
	Scanners    []string  `json:"scanners"`
	StartedAt   time.Time `json:"started_at"`
	CompletedAt time.Time `json:"completed_at,omitempty"`
	Error       string    `json:"error,omitempty"`
	DryRun      bool      `json:"dry_run,omitempty"`
	// Per-scanner status
	ScannerStatuses []ScannerJobStatus `json:"scanner_statuses"`
	// Scan context — what was scanned and how
	ScanContext *ScanContext `json:"scan_context,omitempty"`
}

ScanJob represents a scan execution job

func (*ScanJob) MarshalBinary

func (j *ScanJob) MarshalBinary() ([]byte, error)

MarshalBinary implements encoding.BinaryMarshaler

func (*ScanJob) UnmarshalBinary

func (j *ScanJob) UnmarshalBinary(data []byte) error

UnmarshalBinary implements encoding.BinaryUnmarshaler

type ScanJobSummary

type ScanJobSummary struct {
	ID            string    `json:"id"`
	ServerName    string    `json:"server_name"`
	Status        string    `json:"status"`
	ScanPass      int       `json:"scan_pass"`
	StartedAt     time.Time `json:"started_at"`
	CompletedAt   time.Time `json:"completed_at,omitempty"`
	FindingsCount int       `json:"findings_count"`
	RiskScore     int       `json:"risk_score"`
	Scanners      []string  `json:"scanners"`
}

ScanJobSummary is a lightweight view of a scan job for history listing

type ScanQueue

type ScanQueue struct {
	// contains filtered or unexported fields
}

ScanQueue manages a queue of scan requests with a worker pool

func NewScanQueue

func NewScanQueue(logger *zap.Logger) *ScanQueue

NewScanQueue creates a new scan queue

func (*ScanQueue) CancelAll

func (q *ScanQueue) CancelAll() error

CancelAll cancels the current batch scan

func (*ScanQueue) GetProgress

func (q *ScanQueue) GetProgress() *QueueProgress

GetProgress returns the current batch scan progress

func (*ScanQueue) IsRunning

func (q *ScanQueue) IsRunning() bool

IsRunning returns true if a batch scan is in progress

func (*ScanQueue) StartScanAll

func (q *ScanQueue) StartScanAll(
	serverList []ServerStatus,
	scanFunc func(ctx context.Context, serverName string) (*ScanJob, error),
) (*QueueProgress, error)

StartScanAll begins scanning all eligible servers using a worker pool. scanFunc is called for each server to perform the actual scan. serverList provides the list of servers to scan.

type ScanReport

type ScanReport struct {
	ID         string          `json:"id"`
	JobID      string          `json:"job_id"`
	ServerName string          `json:"server_name"`
	ScannerID  string          `json:"scanner_id"`
	Findings   []ScanFinding   `json:"findings"`
	RiskScore  int             `json:"risk_score"` // 0-100
	SarifRaw   json.RawMessage `json:"sarif_raw,omitempty"`
	ScannedAt  time.Time       `json:"scanned_at"`
}

ScanReport represents aggregated scan results for a server

func (*ScanReport) MarshalBinary

func (r *ScanReport) MarshalBinary() ([]byte, error)

MarshalBinary implements encoding.BinaryMarshaler

func (*ScanReport) UnmarshalBinary

func (r *ScanReport) UnmarshalBinary(data []byte) error

UnmarshalBinary implements encoding.BinaryUnmarshaler

type ScanRequest

type ScanRequest struct {
	ServerName  string
	SourceDir   string            // Path to server source files (for "source" input)
	DryRun      bool              // If true, don't affect quarantine state
	ScannerIDs  []string          // Specific scanners to use (empty = all installed)
	Env         map[string]string // Additional environment variables
	ScanContext *ScanContext      // Context metadata (set by service)
	ScanPass    int               // 1 = security scan (fast), 2 = supply chain audit (background)
}

ScanRequest describes a scan to execute

type ScanSummary

type ScanSummary struct {
	LastScanAt    *time.Time     `json:"last_scan_at,omitempty"`
	RiskScore     int            `json:"risk_score"`
	Status        string         `json:"status"` // clean, warnings, dangerous, failed, not_scanned, scanning
	FindingCounts *FindingCounts `json:"finding_counts,omitempty"`
}

ScanSummary is a compact representation of scan status for the server list.

type ScannerJobStatus

type ScannerJobStatus struct {
	ScannerID     string    `json:"scanner_id"`
	Status        string    `json:"status"`
	StartedAt     time.Time `json:"started_at,omitempty"`
	CompletedAt   time.Time `json:"completed_at,omitempty"`
	Error         string    `json:"error,omitempty"`
	FindingsCount int       `json:"findings_count"`
	Stdout        string    `json:"stdout,omitempty"` // Scanner stdout (for log viewing)
	Stderr        string    `json:"stderr,omitempty"` // Scanner stderr (for log viewing)
	ExitCode      int       `json:"exit_code"`
}

ScannerJobStatus tracks a single scanner's execution within a scan job

type ScannerPlugin

type ScannerPlugin struct {
	ID          string           `json:"id"`
	Name        string           `json:"name"`
	Vendor      string           `json:"vendor"`
	Description string           `json:"description"`
	License     string           `json:"license"`
	Homepage    string           `json:"homepage"`
	DockerImage string           `json:"docker_image"`
	Inputs      []string         `json:"inputs"`  // "source", "mcp_connection", "container_image"
	Outputs     []string         `json:"outputs"` // "sarif"
	RequiredEnv []EnvRequirement `json:"required_env"`
	OptionalEnv []EnvRequirement `json:"optional_env"`
	Command     []string         `json:"command"`
	Timeout     string           `json:"timeout"`
	NetworkReq  bool             `json:"network_required"`
	// Runtime state (not in registry)
	Status        string            `json:"status"` // available, installed, configured, error
	InstalledAt   time.Time         `json:"installed_at,omitempty"`
	ConfiguredEnv map[string]string `json:"configured_env,omitempty"` // Set env values (secrets redacted in API)
	ImageOverride string            `json:"image_override,omitempty"` // User override for DockerImage
	LastUsedAt    time.Time         `json:"last_used_at,omitempty"`
	ErrorMsg      string            `json:"error_message,omitempty"`
	Custom        bool              `json:"custom,omitempty"` // User-added (not from registry)
}

ScannerPlugin represents a security scanner plugin

func (*ScannerPlugin) EffectiveImage

func (s *ScannerPlugin) EffectiveImage() string

EffectiveImage returns ImageOverride if set, otherwise DockerImage.

func (*ScannerPlugin) MarshalBinary

func (s *ScannerPlugin) MarshalBinary() ([]byte, error)

MarshalBinary implements encoding.BinaryMarshaler

func (*ScannerPlugin) UnmarshalBinary

func (s *ScannerPlugin) UnmarshalBinary(data []byte) error

UnmarshalBinary implements encoding.BinaryUnmarshaler

type ScannerRunConfig

type ScannerRunConfig struct {
	ContainerName string            // e.g., "mcpproxy-scanner-mcp-scan-abc123"
	Image         string            // Docker image to use
	Command       []string          // Command to run inside container
	Env           map[string]string // Environment variables
	SourceDir     string            // Host directory to mount at /scan/source (read-only)
	ReportDir     string            // Host directory to mount at /scan/report (writable)
	CacheDir      string            // Host directory for scanner cache (persists between runs)
	NetworkMode   string            // "none", "bridge", or custom network name
	Timeout       time.Duration     // Container execution timeout
	ReadOnly      bool              // Read-only root filesystem
	MemoryLimit   string            // e.g., "512m"
	ExtraMounts   []string          // Additional -v mounts (e.g., "~/.claude:/app/.claude:ro")
}

ScannerRunConfig defines how to run a scanner container

type SecretResolverFunc

type SecretResolverFunc func(ctx context.Context, ref string) (string, error)

SecretResolverFunc resolves a secret reference like ${keyring:name} to its value

type SecretStore

type SecretStore interface {
	StoreSecret(ctx context.Context, name, value string) error
	ResolveSecret(ctx context.Context, ref string) (string, error)
}

SecretStore allows storing and resolving secrets via the OS keyring

type SecurityOverview

type SecurityOverview struct {
	TotalScans         int           `json:"total_scans"`
	ActiveScans        int           `json:"active_scans"`
	FindingsBySeverity ReportSummary `json:"findings_by_severity"`
	ScannersInstalled  int           `json:"scanners_installed"`
	ScannersEnabled    int           `json:"scanners_enabled"` // Subset of installed that the engine will run (status installed or configured)
	ServersScanned     int           `json:"servers_scanned"`
	LastScanAt         time.Time     `json:"last_scan_at,omitempty"`
	DockerAvailable    bool          `json:"docker_available"`
}

SecurityOverview provides dashboard aggregate stats

type ServerInfo

type ServerInfo struct {
	Name       string // Server name
	Protocol   string // "stdio", "http", "sse", "streamable-http"
	Command    string // Command used to start the server (stdio only)
	Args       []string
	WorkingDir string            // Configured working directory
	URL        string            // Server URL (HTTP/SSE only)
	Env        map[string]string // Environment variables
}

ServerInfo contains the information needed to resolve a server's source

type ServerInfoProvider

type ServerInfoProvider interface {
	GetServerInfo(serverName string) (*ServerInfo, error)
	GetServerTools(serverName string) ([]map[string]interface{}, error)
	// EnsureConnected attempts to connect a disconnected/quarantined server
	// so that tool definitions can be retrieved for scanning.
	// Returns nil if already connected or successfully connected.
	EnsureConnected(ctx context.Context, serverName string) error
	// IsConnected returns whether the server has an active MCP connection.
	IsConnected(serverName string) bool
}

ServerInfoProvider resolves server configuration for auto-source resolution

type ServerStatus

type ServerStatus struct {
	Name      string
	Enabled   bool
	Connected bool
	Protocol  string
}

ServerStatus provides info about a server for queue filtering

type ServerUnquarantiner

type ServerUnquarantiner interface {
	UnquarantineServer(serverName string) error
}

ServerUnquarantiner performs the full unquarantine workflow for a server. Implementations are expected to:

  • Clear the quarantined flag in storage and persist config
  • Trigger a tool (re)index for the server
  • Emit the same events/activity entries that the normal unquarantine path emits

This interface is intentionally small so the scanner service does not need to depend on the full runtime package.

type Service

type Service struct {
	// contains filtered or unexported fields
}

Service coordinates scanner management, scan execution, and approval workflow

func NewService

func NewService(storage Storage, registry *Registry, docker *DockerRunner, dataDir string, logger *zap.Logger) *Service

NewService creates a new SecurityService

func (*Service) ApproveServer

func (s *Service) ApproveServer(ctx context.Context, serverName string, force bool, approvedBy string) error

ApproveServer approves a scanned server, storing the integrity baseline

func (*Service) CancelAllScans

func (s *Service) CancelAllScans() error

CancelAllScans cancels the current batch scan

func (*Service) CancelScan

func (s *Service) CancelScan(ctx context.Context, serverName string) error

CancelScan cancels a running scan for a server

func (*Service) CheckIntegrity

func (s *Service) CheckIntegrity(ctx context.Context, serverName string) (*IntegrityCheckResult, error)

CheckIntegrity verifies a server's runtime integrity against its baseline

func (*Service) CleanupStaleJobs

func (s *Service) CleanupStaleJobs()

CleanupStaleJobs marks any running/pending scan jobs as failed. Called on startup to clean up jobs that were interrupted by a process crash.

func (*Service) ConfigureScanner

func (s *Service) ConfigureScanner(_ context.Context, id string, env map[string]string, dockerImage string) error

ConfigureScanner sets environment variables (API keys) and/or an image override for a scanner.

Scanner env values are stored DIRECTLY in the scanner's ConfiguredEnv map in BBolt, NOT in the OS keyring. Previous versions attempted to write to the OS keyring and fall back on failure, but that path is unsafe on macOS: keyring.Set (which wraps Security.framework under the hood) can pop a blocking "Keychain Not Found" system modal when the user's default keychain is in an unusual state, and the underlying goroutine cannot be cancelled once started (it stays live until it hits the real backend, which may never happen). The scanner env values end up in the container's /proc/environ at scan time anyway, so keyring storage adds no meaningful confidentiality — it's a trust-boundary we don't actually have.

Users who want OS-keyring storage for a specific secret can still use a `${keyring:my-secret-name}` reference as the env value. The resolver expands it at scan time via a read-only keyring Get, which is safe on all platforms.

If the effective Docker image changes (user set a new override) and the new image is not already cached locally, the scanner is transitioned to the "pulling" state and a background pull is kicked off. The call returns immediately — the UI tracks the pull via SSE or polling.

func (*Service) GetOverview

func (s *Service) GetOverview(ctx context.Context) (*SecurityOverview, error)

GetOverview returns aggregated security statistics

func (*Service) GetQueueProgress

func (s *Service) GetQueueProgress() *QueueProgress

GetQueueProgress returns the current batch scan progress

func (*Service) GetScanReport

func (s *Service) GetScanReport(ctx context.Context, serverName string) (*AggregatedReport, error)

GetScanReport returns the aggregated report for a server, merging both Pass 1 and Pass 2 results.

func (*Service) GetScanReportByJobID

func (s *Service) GetScanReportByJobID(ctx context.Context, jobID string) (*AggregatedReport, error)

GetScanReportByJobID returns an aggregated report for a specific scan job ID.

func (*Service) GetScanStatus

func (s *Service) GetScanStatus(ctx context.Context, serverName string) (*ScanJob, error)

GetScanStatus returns the current scan status for a server. Prefers Pass 1 (security scan) which contains the primary scanner execution data. Pass 2 (supply chain audit) is only returned if Pass 1 is not available.

func (*Service) GetScanStatusByPass

func (s *Service) GetScanStatusByPass(ctx context.Context, serverName string, pass int) (*ScanJob, error)

GetScanStatusByPass returns the scan job for a specific pass (1=security, 2=supply chain). If pass is 0 or not found, falls back to GetScanStatus behavior (latest job).

func (*Service) GetScanSummary

func (s *Service) GetScanSummary(ctx context.Context, serverName string) *ScanSummary

GetScanSummary returns a compact scan summary for a server (for the server list API). Returns nil if no scans have been run for this server. Considers both Pass 1 and Pass 2 results when computing status.

func (*Service) GetScanner

func (s *Service) GetScanner(ctx context.Context, id string) (*ScannerPlugin, error)

GetScanner returns a scanner by ID

func (*Service) GetScannerStatus

func (s *Service) GetScannerStatus(ctx context.Context, id string) (*ScannerPlugin, error)

GetScannerStatus returns the current status of a scanner

func (*Service) GetSecurityOverview

func (s *Service) GetSecurityOverview(ctx context.Context) (*SecurityOverview, error)

GetSecurityOverview returns aggregated security statistics. Satisfies the httpapi.SecurityController interface.

func (*Service) InstallScanner

func (s *Service) InstallScanner(ctx context.Context, id string) error

InstallScanner enables a scanner and kicks off a background Docker image pull. Returns immediately — the UI tracks progress via SSE events (security.scanner_changed) or by polling GET /api/v1/security/scanners.

Behavior:

  1. If the image is already present locally, the scanner is marked "installed" synchronously and the function returns nil.
  2. Otherwise the scanner is marked "pulling" and a goroutine performs the actual docker pull. On success status → "installed"; on failure status → "error" with ErrorMsg set.
  3. If Docker itself is not running, the scanner is marked "error" so the user gets clear feedback.

func (*Service) IsQueueRunning

func (s *Service) IsQueueRunning() bool

IsQueueRunning returns true if a batch scan is in progress

func (*Service) ListScanHistory

func (s *Service) ListScanHistory(ctx context.Context) ([]ScanJobSummary, error)

ListScanHistory returns all scan jobs as summaries, enriched with findings count and risk score.

func (*Service) ListScanners

func (s *Service) ListScanners(ctx context.Context) ([]*ScannerPlugin, error)

ListScanners returns all scanners from registry merged with installed state from storage

func (*Service) RejectServer

func (s *Service) RejectServer(ctx context.Context, serverName string) error

RejectServer rejects a server, cleaning up all artifacts

func (*Service) RemoveScanner

func (s *Service) RemoveScanner(ctx context.Context, id string) error

RemoveScanner removes a scanner, its Docker image, and stored configuration. If a background pull is in progress for this scanner it is cancelled.

func (*Service) ScanAll

func (s *Service) ScanAll(ctx context.Context, servers []ServerStatus, scannerIDs []string) (*QueueProgress, error)

ScanAll starts scanning all eligible servers using the worker pool. Disabled servers are skipped with a reason.

func (*Service) SetEmitter

func (s *Service) SetEmitter(emitter EventEmitter)

SetEmitter sets the event emitter for the service.

func (*Service) SetSecretStore

func (s *Service) SetSecretStore(store SecretStore)

SetSecretStore sets the secret store for secure API key management. Also wires secret resolution into the scan engine for resolving ${keyring:...} references in scanner env vars at scan time.

func (*Service) SetServerInfoProvider

func (s *Service) SetServerInfoProvider(provider ServerInfoProvider)

SetServerInfoProvider sets the provider for resolving server configuration

func (*Service) SetServerUnquarantiner

func (s *Service) SetServerUnquarantiner(u ServerUnquarantiner)

SetServerUnquarantiner wires the unquarantine callback used by ApproveServer. If not set, ApproveServer will still succeed in storing a baseline but will log a warning because it cannot actually unquarantine the server.

func (*Service) StartScan

func (s *Service) StartScan(ctx context.Context, serverName string, dryRun bool, scannerIDs []string, sourceDir string) (*ScanJob, error)

StartScan triggers a security scan for a server (Pass 1: fast security scan). After Pass 1 completes, Pass 2 (supply chain audit) is auto-started in the background.

type SourceResolver

type SourceResolver struct {
	// contains filtered or unexported fields
}

SourceResolver automatically determines the source directory for scanning a server. It resolves source based on server type:

  • Docker-isolated stdio servers: extracts changed files from running container
  • HTTP/SSE servers: no source needed (scanners use mcp_connection)
  • Local stdio servers: uses working_dir or command directory

func NewSourceResolver

func NewSourceResolver(logger *zap.Logger) *SourceResolver

NewSourceResolver creates a new SourceResolver

func (*SourceResolver) EnrichWithFileList

func (r *SourceResolver) EnrichWithFileList(resolved *ResolvedSource)

EnrichWithFileList populates the Files, TotalFiles, TotalSize fields on a ResolvedSource

func (*SourceResolver) Resolve

func (r *SourceResolver) Resolve(ctx context.Context, info ServerInfo) (*ResolvedSource, error)

Resolve determines the source directory for scanning a server. It tries these strategies in order:

  1. Find running Docker container for the server (mcpproxy-<name>-*)
  2. Use working_dir from server config
  3. Use directory containing the server command
  4. For HTTP servers, return URL for mcp_connection scanners

func (*SourceResolver) ResolveFullSource

func (r *SourceResolver) ResolveFullSource(ctx context.Context, info ServerInfo) (*ResolvedSource, error)

ResolveFullSource resolves the FULL source directory for a server, including all dependencies (site-packages, node_modules, UV archives, etc.). This is used for Pass 2 (supply chain audit) to scan the complete filesystem.

type Storage

type Storage interface {
	SaveScanner(s *ScannerPlugin) error
	GetScanner(id string) (*ScannerPlugin, error)
	ListScanners() ([]*ScannerPlugin, error)
	DeleteScanner(id string) error

	SaveScanJob(job *ScanJob) error
	GetScanJob(id string) (*ScanJob, error)
	ListScanJobs(serverName string) ([]*ScanJob, error)
	GetLatestScanJob(serverName string) (*ScanJob, error)
	DeleteScanJob(id string) error
	DeleteServerScanJobs(serverName string) error

	SaveScanReport(report *ScanReport) error
	GetScanReport(id string) (*ScanReport, error)
	ListScanReports(serverName string) ([]*ScanReport, error)
	ListScanReportsByJob(jobID string) ([]*ScanReport, error)
	DeleteScanReport(id string) error
	DeleteServerScanReports(serverName string) error

	SaveIntegrityBaseline(baseline *IntegrityBaseline) error
	GetIntegrityBaseline(serverName string) (*IntegrityBaseline, error)
	DeleteIntegrityBaseline(serverName string) error
	ListIntegrityBaselines() ([]*IntegrityBaseline, error)
}

Storage defines the storage interface needed by SecurityService

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL