bab29a83fa
Sweep all .go imports + go.mod, plus module-path refs in .golangci.yml, Dockerfile, swagger templates, locale example, and repo/wiki URLs (MokoGitea-Fork -> MokoGIT). Part of the MokoGIT rebrand. Claude-Session: https://claude.ai/code/session_01Bqe7fAuHQeiLueYfeHFrHw
220 lines
8.2 KiB
Go
220 lines
8.2 KiB
Go
// Copyright 2026 Moko Consulting <hello@mokoconsulting.tech>
|
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
|
|
package security
|
|
|
|
import (
|
|
"context"
|
|
|
|
"code.mokoconsulting.tech/MokoConsulting/MokoGIT/models/db"
|
|
"code.mokoconsulting.tech/MokoConsulting/MokoGIT/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
|
|
}
|