Documentation
¶
Index ¶
- Constants
- func CalculateRiskScore(findings []ScanFinding) int
- func ClassifyAllFindings(findings []ScanFinding)
- func ClassifyThreat(f *ScanFinding)
- func CollectFileList(dir string) (files []string, totalFiles int, totalSize int64)
- func GenerateContainerName(scannerID, serverName string) string
- func IsSARIF(data []byte) bool
- func PrepareReportDir(baseDir, jobID, scannerID string) (string, error)
- func ValidateManifest(s *ScannerPlugin) error
- type AggregatedReport
- type DockerRunner
- func (d *DockerRunner) GetImageDigest(ctx context.Context, image string) (string, error)
- func (d *DockerRunner) ImageExists(ctx context.Context, image string) bool
- func (d *DockerRunner) IsDockerAvailable(ctx context.Context) bool
- func (d *DockerRunner) KillContainer(ctx context.Context, name string) error
- func (d *DockerRunner) PullImage(ctx context.Context, image string) error
- func (d *DockerRunner) ReadReportFile(reportDir string) ([]byte, error)
- func (d *DockerRunner) RemoveImage(ctx context.Context, image string) error
- func (d *DockerRunner) RunScanner(ctx context.Context, cfg ScannerRunConfig) (stdout, stderr string, exitCode int, err error)
- func (d *DockerRunner) StopContainer(ctx context.Context, name string, timeout int) error
- type Engine
- type EnvRequirement
- type EventEmitter
- type FindingCounts
- type IntegrityBaseline
- type IntegrityCheckResult
- type IntegrityViolation
- type NoopCallback
- func (n *NoopCallback) OnScanCompleted(_ *ScanJob, _ []*ScanReport)
- func (n *NoopCallback) OnScanFailed(_ *ScanJob, _ error)
- func (n *NoopCallback) OnScanStarted(_ *ScanJob)
- func (n *NoopCallback) OnScannerCompleted(_ *ScanJob, _ string, _ *ScanReport)
- func (n *NoopCallback) OnScannerFailed(_ *ScanJob, _ string, _ error)
- func (n *NoopCallback) OnScannerStarted(_ *ScanJob, _ string)
- type NoopEmitter
- func (n *NoopEmitter) EmitSecurityIntegrityAlert(string, string, string)
- func (n *NoopEmitter) EmitSecurityScanCompleted(string, map[string]int)
- func (n *NoopEmitter) EmitSecurityScanFailed(string, string, string)
- func (n *NoopEmitter) EmitSecurityScanProgress(string, string, string, int)
- func (n *NoopEmitter) EmitSecurityScanStarted(string, []string, string)
- func (n *NoopEmitter) EmitSecurityScannerChanged(string, string, string)
- type QueueItem
- type QueueProgress
- type Registry
- type ReportSummary
- type ResolvedSource
- type SARIFArtifactLocation
- type SARIFConfiguration
- type SARIFDriver
- type SARIFLocation
- type SARIFMessage
- type SARIFPhysicalLocation
- type SARIFRegion
- type SARIFReport
- type SARIFResult
- type SARIFRule
- type SARIFRun
- type SARIFTool
- type ScanAllRequest
- type ScanCallback
- type ScanContext
- type ScanFinding
- type ScanJob
- type ScanJobSummary
- type ScanQueue
- type ScanReport
- type ScanRequest
- type ScanSummary
- type ScannerJobStatus
- type ScannerPlugin
- type ScannerRunConfig
- type SecretResolverFunc
- type SecretStore
- type SecurityOverview
- type ServerInfo
- type ServerInfoProvider
- type ServerStatus
- type ServerUnquarantiner
- type Service
- func (s *Service) ApproveServer(ctx context.Context, serverName string, force bool, approvedBy string) error
- func (s *Service) CancelAllScans() error
- func (s *Service) CancelScan(ctx context.Context, serverName string) error
- func (s *Service) CheckIntegrity(ctx context.Context, serverName string) (*IntegrityCheckResult, error)
- func (s *Service) CleanupStaleJobs()
- func (s *Service) ConfigureScanner(_ context.Context, id string, env map[string]string, dockerImage string) error
- func (s *Service) GetOverview(ctx context.Context) (*SecurityOverview, error)
- func (s *Service) GetQueueProgress() *QueueProgress
- func (s *Service) GetScanReport(ctx context.Context, serverName string) (*AggregatedReport, error)
- func (s *Service) GetScanReportByJobID(ctx context.Context, jobID string) (*AggregatedReport, error)
- func (s *Service) GetScanStatus(ctx context.Context, serverName string) (*ScanJob, error)
- func (s *Service) GetScanStatusByPass(ctx context.Context, serverName string, pass int) (*ScanJob, error)
- func (s *Service) GetScanSummary(ctx context.Context, serverName string) *ScanSummary
- func (s *Service) GetScanner(ctx context.Context, id string) (*ScannerPlugin, error)
- func (s *Service) GetScannerStatus(ctx context.Context, id string) (*ScannerPlugin, error)
- func (s *Service) GetSecurityOverview(ctx context.Context) (*SecurityOverview, error)
- func (s *Service) InstallScanner(ctx context.Context, id string) error
- func (s *Service) IsQueueRunning() bool
- func (s *Service) ListScanHistory(ctx context.Context) ([]ScanJobSummary, error)
- func (s *Service) ListScanners(ctx context.Context) ([]*ScannerPlugin, error)
- func (s *Service) RejectServer(ctx context.Context, serverName string) error
- func (s *Service) RemoveScanner(ctx context.Context, id string) error
- func (s *Service) ScanAll(ctx context.Context, servers []ServerStatus, scannerIDs []string) (*QueueProgress, error)
- func (s *Service) SetEmitter(emitter EventEmitter)
- func (s *Service) SetSecretStore(store SecretStore)
- func (s *Service) SetServerInfoProvider(provider ServerInfoProvider)
- func (s *Service) SetServerUnquarantiner(u ServerUnquarantiner)
- func (s *Service) StartScan(ctx context.Context, serverName string, dryRun bool, scannerIDs []string, ...) (*ScanJob, error)
- type SourceResolver
- type Storage
Constants ¶
const ( DefaultWorkerPoolSize = 3 // Max concurrent scans MaxQueueSize = 50 // Max pending scans in queue )
const ( QueueStatusPending = "pending" QueueStatusRunning = "running" QueueStatusCompleted = "completed" QueueStatusFailed = "failed" QueueStatusSkipped = "skipped" QueueStatusCancelled = "cancelled" )
QueueItemStatus represents the status of a queued scan
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
const ( ScanJobStatusPending = "pending" ScanJobStatusRunning = "running" ScanJobStatusCompleted = "completed" ScanJobStatusFailed = "failed" ScanJobStatusCancelled = "cancelled" )
Scan job status constants
const ( SeverityCritical = "critical" SeverityHigh = "high" SeverityMedium = "medium" SeverityLow = "low" SeverityInfo = "info" )
Scan finding severity constants
const ( ScanPassSecurityScan = 1 // Fast pass: source code + lockfile scanning ScanPassSupplyChainAudit = 2 // Background pass: full filesystem CVE analysis )
Scan pass constants
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
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
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 ¶
CollectFileList walks a directory and returns a list of files (relative paths). Caps at MaxScannedFiles entries. Also returns total count and size.
func GenerateContainerName ¶
GenerateContainerName creates a unique container name for a scanner run
func PrepareReportDir ¶
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 ¶
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 ¶
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 ¶
CancelScan cancels a running scan for a server
func (*Engine) GetActiveJob ¶
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 ¶
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 ¶
Unregister removes a custom scanner from the registry Cannot unregister bundled scanners
func (*Registry) UpdateStatus ¶
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 ¶
MarshalBinary implements encoding.BinaryMarshaler
func (*ScanJob) UnmarshalBinary ¶
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 ¶
NewScanQueue creates a new scan queue
func (*ScanQueue) GetProgress ¶
func (q *ScanQueue) GetProgress() *QueueProgress
GetProgress returns the current batch scan 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 ¶
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 ¶
ServerStatus provides info about a server for queue filtering
type ServerUnquarantiner ¶
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 ¶
CancelAllScans cancels the current batch scan
func (*Service) CancelScan ¶
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 ¶
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 ¶
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 ¶
GetScanner returns a scanner by ID
func (*Service) GetScannerStatus ¶
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 ¶
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:
- If the image is already present locally, the scanner is marked "installed" synchronously and the function returns nil.
- 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.
- If Docker itself is not running, the scanner is marked "error" so the user gets clear feedback.
func (*Service) IsQueueRunning ¶
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 ¶
RejectServer rejects a server, cleaning up all artifacts
func (*Service) RemoveScanner ¶
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:
- Find running Docker container for the server (mcpproxy-<name>-*)
- Use working_dir from server config
- Use directory containing the server command
- 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