// Copyright 2026 Moko Consulting // SPDX-License-Identifier: GPL-3.0-or-later package security import ( "context" "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/db" "code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/timeutil" ) func init() { db.RegisterModel(new(SecurityAlert)) db.RegisterModel(new(SecurityScannerConfig)) } // AlertSeverity represents the severity level of a security finding. type AlertSeverity string const ( SeverityCritical AlertSeverity = "critical" SeverityHigh AlertSeverity = "high" SeverityMedium AlertSeverity = "medium" SeverityLow AlertSeverity = "low" SeverityInfo AlertSeverity = "info" ) // AlertStatus represents the lifecycle state of an alert. type AlertStatus string const ( AlertStatusActive AlertStatus = "active" AlertStatusResolved AlertStatus = "resolved" AlertStatusDismissed AlertStatus = "dismissed" ) // ScannerType identifies which scanner produced a finding. type ScannerType string const ( ScannerSecret ScannerType = "secret" ScannerDependency ScannerType = "dependency" ScannerCode ScannerType = "code" ScannerConfig ScannerType = "config" ScannerLicense ScannerType = "license" ) // SecurityAlert stores a single security finding for a repository. type SecurityAlert struct { ID int64 `xorm:"pk autoincr"` RepoID int64 `xorm:"INDEX NOT NULL 'repo_id'"` Scanner ScannerType `xorm:"VARCHAR(20) NOT NULL 'scanner'"` Severity AlertSeverity `xorm:"VARCHAR(10) NOT NULL 'severity'"` Status AlertStatus `xorm:"VARCHAR(10) NOT NULL DEFAULT 'active' 'status'"` RuleID string `xorm:"VARCHAR(100) NOT NULL 'rule_id'"` // e.g. "aws-access-key", "cve-2024-1234" Title string `xorm:"TEXT NOT NULL 'title'"` Description string `xorm:"TEXT 'description'"` FilePath string `xorm:"TEXT 'file_path'"` LineNumber int `xorm:"'line_number'"` CommitSHA string `xorm:"VARCHAR(64) 'commit_sha'"` Fingerprint string `xorm:"VARCHAR(64) INDEX 'fingerprint'"` // dedup key: hash of rule+file+content Metadata string `xorm:"TEXT 'metadata'"` // JSON extra data ResolvedBy int64 `xorm:"'resolved_by'"` CreatedUnix timeutil.TimeStamp `xorm:"INDEX CREATED 'created_unix'"` UpdatedUnix timeutil.TimeStamp `xorm:"UPDATED 'updated_unix'"` } func (SecurityAlert) TableName() string { return "security_alert" } // SecurityScannerConfig stores per-repo scanner settings. type SecurityScannerConfig struct { ID int64 `xorm:"pk autoincr"` RepoID int64 `xorm:"UNIQUE INDEX NOT NULL 'repo_id'"` Enabled bool `xorm:"NOT NULL DEFAULT true 'enabled'"` BlockOnPush bool `xorm:"NOT NULL DEFAULT false 'block_on_push'"` // reject push if secrets found SecretScanner bool `xorm:"NOT NULL DEFAULT true 'secret_scanner'"` DependScanner bool `xorm:"NOT NULL DEFAULT true 'depend_scanner'"` CodeScanner bool `xorm:"NOT NULL DEFAULT false 'code_scanner'"` ConfigScanner bool `xorm:"NOT NULL DEFAULT false 'config_scanner'"` LicenseScanner bool `xorm:"NOT NULL DEFAULT false 'license_scanner'"` CustomPatterns string `xorm:"TEXT 'custom_patterns'"` // JSON array of custom regex patterns CreatedUnix timeutil.TimeStamp `xorm:"INDEX CREATED 'created_unix'"` UpdatedUnix timeutil.TimeStamp `xorm:"UPDATED 'updated_unix'"` } func (SecurityScannerConfig) TableName() string { return "security_scanner_config" } // ────────────────────────────────────────────────────────────────────── // Alert queries // ────────────────────────────────────────────────────────────────────── // GetActiveAlerts returns all active alerts for a repo. func GetActiveAlerts(ctx context.Context, repoID int64) ([]*SecurityAlert, error) { alerts := make([]*SecurityAlert, 0, 20) return alerts, db.GetEngine(ctx). Where("repo_id = ? AND status = ?", repoID, AlertStatusActive). OrderBy("severity ASC, created_unix DESC"). Find(&alerts) } // GetAllAlerts returns all alerts for a repo (including resolved/dismissed). func GetAllAlerts(ctx context.Context, repoID int64) ([]*SecurityAlert, error) { alerts := make([]*SecurityAlert, 0, 50) return alerts, db.GetEngine(ctx). Where("repo_id = ?", repoID). OrderBy("status ASC, severity ASC, created_unix DESC"). Find(&alerts) } // GetAlertByID returns a single alert. func GetAlertByID(ctx context.Context, id int64) (*SecurityAlert, error) { alert := new(SecurityAlert) has, err := db.GetEngine(ctx).ID(id).Get(alert) if err != nil { return nil, err } if !has { return nil, db.ErrNotExist{Resource: "SecurityAlert", ID: id} } return alert, nil } // GetAlertCountsByRepo returns count of active alerts grouped by severity. func GetAlertCountsByRepo(ctx context.Context, repoID int64) (map[AlertSeverity]int64, error) { type result struct { Severity AlertSeverity `xorm:"severity"` Count int64 `xorm:"count"` } var results []result err := db.GetEngine(ctx). Table("security_alert"). Select("severity, COUNT(*) as count"). Where("repo_id = ? AND status = ?", repoID, AlertStatusActive). GroupBy("severity"). Find(&results) if err != nil { return nil, err } counts := make(map[AlertSeverity]int64) for _, r := range results { counts[r.Severity] = r.Count } return counts, nil } // CreateOrUpdateAlert creates a new alert or updates if fingerprint exists. func CreateOrUpdateAlert(ctx context.Context, alert *SecurityAlert) error { if alert.Fingerprint != "" { existing := new(SecurityAlert) has, err := db.GetEngine(ctx). Where("repo_id = ? AND fingerprint = ?", alert.RepoID, alert.Fingerprint). Get(existing) if err != nil { return err } if has { // Update existing - refresh commit SHA and keep active existing.CommitSHA = alert.CommitSHA existing.LineNumber = alert.LineNumber existing.Status = AlertStatusActive _, err = db.GetEngine(ctx).ID(existing.ID). Cols("commit_sha", "line_number", "status").Update(existing) return err } } _, err := db.GetEngine(ctx).Insert(alert) return err } // UpdateAlertStatus changes the status of an alert. func UpdateAlertStatus(ctx context.Context, id int64, status AlertStatus, resolvedBy int64) error { _, err := db.GetEngine(ctx).ID(id). Cols("status", "resolved_by"). Update(&SecurityAlert{Status: status, ResolvedBy: resolvedBy}) return err } // ────────────────────────────────────────────────────────────────────── // Scanner config queries // ────────────────────────────────────────────────────────────────────── // GetScannerConfig returns the scanner config for a repo, or defaults. func GetScannerConfig(ctx context.Context, repoID int64) (*SecurityScannerConfig, error) { cfg := new(SecurityScannerConfig) has, err := db.GetEngine(ctx).Where("repo_id = ?", repoID).Get(cfg) if err != nil { return nil, err } if !has { return &SecurityScannerConfig{ RepoID: repoID, Enabled: true, SecretScanner: true, DependScanner: true, }, nil } return cfg, nil } // SaveScannerConfig creates or updates scanner config. func SaveScannerConfig(ctx context.Context, cfg *SecurityScannerConfig) error { existing := new(SecurityScannerConfig) has, err := db.GetEngine(ctx).Where("repo_id = ?", cfg.RepoID).Get(existing) if err != nil { return err } if has { cfg.ID = existing.ID _, err = db.GetEngine(ctx).ID(cfg.ID).AllCols().Update(cfg) return err } _, err = db.GetEngine(ctx).Insert(cfg) return err }