diff --git a/engine/checks/checks.go b/engine/checks/checks.go index 96d8146a..4cd8cb60 100644 --- a/engine/checks/checks.go +++ b/engine/checks/checks.go @@ -1,16 +1,19 @@ package checks import ( - "encoding/csv" "errors" - "fmt" "log/slog" "math/rand" - "os" "strings" "time" ) +// TaskCredential represents a credential from the task payload +type TaskCredential struct { + Username string + Password string +} + // checks for each service type Runner interface { Run(teamID uint, identifier string, roundID uint, resultsChan chan Result) @@ -20,24 +23,26 @@ type Runner interface { GetName() string GetAttempts() int GetCredlists() []string + SetTaskCredentials(creds []TaskCredential) } // services will inherit Service so that config.Config can be read from file, but will not be used after initial read type Service struct { - Name string `toml:"-"` // Name is the box name plus the service (ex. lunar-dns) - Display string `toml:",omitempty"` // Display is the name of the service (ex. dns) - CredLists []string `toml:",omitempty"` - Port int `toml:",omitzero"` // omitzero because custom checks might not specify port, and shouldn't be assigned 0 - Points int `toml:",omitempty"` - Timeout int `toml:",omitempty"` - SlaPenalty int `toml:",omitempty"` - SlaThreshold int `toml:",omitempty"` - LaunchTime time.Time `toml:",omitempty"` - StopTime time.Time `toml:",omitempty"` - Disabled bool `toml:",omitempty"` - Target string `toml:",omitempty"` // Target is the IP address or hostname for the box - ServiceType string `toml:",omitempty"` // ServiceType is the name of the Runner that checks the service - Attempts int `toml:",omitempty"` // Attempts is the number of times the service has been checked + Name string `toml:"-"` // Name is the box name plus the service (ex. lunar-dns) + Display string `toml:",omitempty"` // Display is the name of the service (ex. dns) + CredLists []string `toml:",omitempty"` + Port int `toml:",omitzero"` // omitzero because custom checks might not specify port, and shouldn't be assigned 0 + Points int `toml:",omitempty"` + Timeout int `toml:",omitempty"` + SlaPenalty int `toml:",omitempty"` + SlaThreshold int `toml:",omitempty"` + LaunchTime time.Time `toml:",omitempty"` + StopTime time.Time `toml:",omitempty"` + Disabled bool `toml:",omitempty"` + Target string `toml:",omitempty"` // Target is the IP address or hostname for the box + ServiceType string `toml:",omitempty"` // ServiceType is the name of the Runner that checks the service + Attempts int `toml:",omitempty"` // Attempts is the number of times the service has been checked + TaskCredentials []TaskCredential `toml:"-"` // Credentials from task payload (set per-task, not from config) } type Result struct { @@ -78,56 +83,19 @@ func (service *Service) SetCredlists(lists []string) { service.CredLists = lists } -func (service *Service) getCreds(teamID uint) (string, string, error) { - // check if credlists are defined, if not return error - if len(service.CredLists) == 0 { - return "", "", errors.New("no credlists defined") - } - - // pick which list to use - rng := rand.New(rand.NewSource(time.Now().UnixNano())) // #nosec G404 -- non-crypto RNG for credlist selection - credListName := service.CredLists[rng.Intn(len(service.CredLists))] // #nosec G404 -- non-crypto selection of credlist to use - - // get the credlist from the filesystem using os.Root for path safety - baseDir := "submissions/pcrs" - relativePath := fmt.Sprintf("%d/%s", teamID, credListName) - - root, err := os.OpenRoot(baseDir) - if err != nil { - return "", "", fmt.Errorf("failed to open pcrs directory: %w", err) - } - defer func() { - if err := root.Close(); err != nil { - slog.Error("failed to close pcrs root directory", "error", err) - } - }() - - file, err := root.Open(relativePath) - if err != nil { - return "", "", err - } - defer func() { - if err := file.Close(); err != nil { - slog.Error("failed to close credlist file", "error", err) - } - }() - - reader := csv.NewReader(file) - records, err := reader.ReadAll() - if err != nil { - return "", "", err - } +// SetTaskCredentials sets credentials from the task payload (thread-safe per-instance) +func (service *Service) SetTaskCredentials(creds []TaskCredential) { + service.TaskCredentials = creds +} - if len(records) == 0 || len(records[0]) < 2 { - return "", "", errors.New("invalid credlist format") +func (service *Service) getCreds(teamID uint) (string, string, error) { + if len(service.TaskCredentials) == 0 { + return "", "", errors.New("no credentials available") } - randomIndex := rng.Intn(len(records)) - - username := records[randomIndex][0] - password := records[randomIndex][1] - - return username, password, nil + rng := rand.New(rand.NewSource(time.Now().UnixNano())) // #nosec G404 -- non-crypto RNG + randomIndex := rng.Intn(len(service.TaskCredentials)) + return service.TaskCredentials[randomIndex].Username, service.TaskCredentials[randomIndex].Password, nil } func (service *Service) Configure(ip string, points int, timeout int, slapenalty int, slathreshold int) error { diff --git a/engine/checks/custom.go b/engine/checks/custom.go index b77ed4e2..e768ff51 100644 --- a/engine/checks/custom.go +++ b/engine/checks/custom.go @@ -69,7 +69,7 @@ func (c Custom) Run(teamID uint, teamIdentifier string, roundID uint, resultsCha } }() - tmpfileName := fmt.Sprintf("custom-check-%d-%d-%s", roundID, teamID, c.Name) + tmpfileName := fmt.Sprintf("custom-check-%d-%d-%s-%d", roundID, teamID, c.Name, time.Now().UnixNano()) tmpfile, err := tmpRoot.Create(tmpfileName) if err != nil { checkResult.Error = "error creating tmpfile" diff --git a/engine/credentials.go b/engine/credentials.go index b463c894..471e9b66 100644 --- a/engine/credentials.go +++ b/engine/credentials.go @@ -3,10 +3,8 @@ package engine import ( "encoding/csv" "fmt" - "io" "log/slog" "os" - "path/filepath" "quotient/engine/db" "sync" ) @@ -21,61 +19,104 @@ func safeOpenInDir(baseDir, relativePath string) (*os.File, error) { return root.Open(relativePath) } -// safeCreateInDir creates a file within the given base directory safely using os.Root. -func safeCreateInDir(baseDir, relativePath string) (*os.File, error) { - root, err := os.OpenRoot(baseDir) - if err != nil { - return nil, fmt.Errorf("failed to open root directory: %w", err) - } - defer root.Close() - return root.Create(relativePath) -} - -func (se *ScoringEngine) LoadCredentials() error { +func (se *ScoringEngine) EnsureCredentialsSeeded() error { teams, err := db.GetTeams() if err != nil { return fmt.Errorf("failed to get teams: %v", err) } + // Initialize mutex map for _, team := range teams { se.CredentialsMutex[team.ID] = &sync.Mutex{} + } - // Iterate directly over the credlists defined in the config + // Check if credentials are already seeded in DB + seeded, err := db.IsCredentialsSeeded() + if err != nil { + return fmt.Errorf("failed to check if credentials are seeded: %v", err) + } + + if !seeded { + slog.Info("Seeding credentials from config files to database") + // Seed original credentials from files for _, configCredlist := range se.Config.CredlistSettings.Credlist { credlistPath := configCredlist.CredlistPath - // Ensure the source file exists - sourcePath := fmt.Sprintf("config/credlists/%s", credlistPath) - if _, err := os.Stat(sourcePath); os.IsNotExist(err) { - return fmt.Errorf("credlist file %s defined in config does not exist: %v", sourcePath, err) - } else if err != nil { - return fmt.Errorf("failed to check credlist file %s: %v", sourcePath, err) + file, err := safeOpenInDir("config/credlists", credlistPath) + if err != nil { + return fmt.Errorf("failed to open credlist file %s: %v", credlistPath, err) + } + + reader := csv.NewReader(file) + records, err := reader.ReadAll() + if closeErr := file.Close(); closeErr != nil { + slog.Error("failed to close credlist file", "path", credlistPath, "error", closeErr) } + if err != nil { + return fmt.Errorf("failed to read credlist file %s: %v", credlistPath, err) + } + + if err := db.SeedOriginalCredentials(credlistPath, records); err != nil { + return fmt.Errorf("failed to seed original credentials for %s: %v", credlistPath, err) + } + } - submissionPath := fmt.Sprintf("submissions/pcrs/%d/%s", team.ID, credlistPath) - if _, err := os.Stat(submissionPath); os.IsNotExist(err) { - destDir := filepath.Dir(submissionPath) - if err := os.MkdirAll(destDir, 0750); err != nil { - return fmt.Errorf("failed to create directory %s: %v", destDir, err) + // Seed team credentials from originals + for _, team := range teams { + if err := db.SeedTeamCredentials(team.ID); err != nil { + return fmt.Errorf("failed to seed credentials for team %d: %v", team.ID, err) + } + } + slog.Info("Credentials seeded successfully") + } else { + slog.Info("Credentials already seeded in database") + // Check for new teams that need seeding + for _, team := range teams { + creds, err := db.GetAllTeamCredentials(team.ID) + if err != nil { + return fmt.Errorf("failed to get credentials for team %d: %v", team.ID, err) + } + if len(creds) == 0 { + slog.Info("Seeding credentials for new team", "team_id", team.ID) + if err := db.SeedTeamCredentials(team.ID); err != nil { + return fmt.Errorf("failed to seed credentials for team %d: %v", team.ID, err) } + } + } - sourceFile, err := safeOpenInDir("config/credlists", credlistPath) + // Check for new credlists that need seeding + for _, configCredlist := range se.Config.CredlistSettings.Credlist { + credlistPath := configCredlist.CredlistPath + origCreds, err := db.GetOriginalCredentials(credlistPath) + if err != nil { + return fmt.Errorf("failed to check original credentials for %s: %v", credlistPath, err) + } + if len(origCreds) == 0 { + slog.Info("Seeding new credlist", "credlist", credlistPath) + file, err := safeOpenInDir("config/credlists", credlistPath) if err != nil { - return fmt.Errorf("failed to open source file %s: %v", credlistPath, err) + return fmt.Errorf("failed to open credlist file %s: %v", credlistPath, err) } - defer sourceFile.Close() - destFile, err := safeCreateInDir(fmt.Sprintf("submissions/pcrs/%d", team.ID), credlistPath) + reader := csv.NewReader(file) + records, err := reader.ReadAll() + if closeErr := file.Close(); closeErr != nil { + slog.Error("failed to close credlist file", "path", credlistPath, "error", closeErr) + } if err != nil { - return fmt.Errorf("failed to create destination file %s: %v", credlistPath, err) + return fmt.Errorf("failed to read credlist file %s: %v", credlistPath, err) + } + + if err := db.SeedOriginalCredentials(credlistPath, records); err != nil { + return fmt.Errorf("failed to seed original credentials for %s: %v", credlistPath, err) } - defer destFile.Close() - if _, err := io.Copy(destFile, sourceFile); err != nil { - return fmt.Errorf("failed to copy credlist file %s: %v", credlistPath, err) + // Seed to all teams + for _, team := range teams { + if err := db.SeedTeamCredentials(team.ID); err != nil { + return fmt.Errorf("failed to seed credentials for team %d: %v", team.ID, err) + } } - } else if err != nil { - return fmt.Errorf("failed to check file %s: %v", submissionPath, err) } } } @@ -83,8 +124,8 @@ func (se *ScoringEngine) LoadCredentials() error { return nil } -func (se *ScoringEngine) UpdateCredentials(teamID uint, credlistName string, usernames []string, passwords []string) (int, error) { - // check if the credlist name is in the config +func (se *ScoringEngine) UpdateCredentials(teamID uint, credlistName string, usernames []string, passwords []string) (int, []string, error) { + // Validate credlist name validCredlist := false for _, c := range se.Config.CredlistSettings.Credlist { if c.CredlistPath == credlistName { @@ -93,7 +134,7 @@ func (se *ScoringEngine) UpdateCredentials(teamID uint, credlistName string, use } } if !validCredlist { - return 0, fmt.Errorf("invalid credlist name") + return 0, nil, fmt.Errorf("invalid credlist name") } se.CredentialsMutex[teamID].Lock() @@ -102,68 +143,26 @@ func (se *ScoringEngine) UpdateCredentials(teamID uint, credlistName string, use slog.Debug("updating credentials", "teamID", teamID, "credlistName", credlistName) if len(usernames) != len(passwords) { - return 0, fmt.Errorf("mismatched usernames and passwords") - } - - originalCreds := make(map[string]string) - credlist, err := safeOpenInDir(fmt.Sprintf("submissions/pcrs/%d", teamID), credlistName) - if err != nil { - return 0, fmt.Errorf("failed to read original credlist: %v", err) - } - - reader := csv.NewReader(credlist) - records, err := reader.ReadAll() - if err != nil { - return 0, fmt.Errorf("failed to read original credlist: %v", err) - } - - for _, record := range records { - if len(record) != 2 { - slog.Debug("invalid credlist format", "record", record) - return 0, fmt.Errorf("invalid credlist format") - } - originalCreds[record[0]] = record[1] + return 0, nil, fmt.Errorf("mismatched usernames and passwords") } updatedCount := 0 - for i, username := range usernames { - if _, exists := originalCreds[username]; !exists { - slog.Debug("username not found in original credlist, skipping update", "username", username) - } else { - originalCreds[username] = passwords[i] - updatedCount++ - } - } - - if err := credlist.Close(); err != nil { - slog.Error("failed to close credlist file", "error", err) - } - - credlistFile, err := safeCreateInDir(fmt.Sprintf("submissions/pcrs/%d", teamID), credlistName) - if err != nil { - return 0, fmt.Errorf("failed to open credlist file for writing: %v", err) - } - defer func() { - if err := credlistFile.Close(); err != nil { - slog.Error("failed to close credlist file", "error", err) - } - }() + var skippedUsernames []string + changedBy := fmt.Sprintf("team%d", teamID) - writer := csv.NewWriter(credlistFile) - for username, password := range originalCreds { - // csv write encoded - if err := writer.Write([]string{username, password}); err != nil { - return 0, fmt.Errorf("failed to write to credlist file: %v", err) + for i, username := range usernames { + err := db.UpdateCredential(teamID, credlistName, username, passwords[i], changedBy) + if err != nil { + if err.Error() == "credential not found" { + skippedUsernames = append(skippedUsernames, username) + continue + } + return updatedCount, skippedUsernames, err } - slog.Debug("successfully wrote to credlist", "username", username, "password", password) + updatedCount++ } - writer.Flush() - if err := writer.Error(); err != nil { - return 0, fmt.Errorf("failed to flush pcr writer: %v", err) - } - - return updatedCount, nil + return updatedCount, skippedUsernames, nil } func (se *ScoringEngine) GetCredlists() (any, error) { @@ -179,33 +178,24 @@ func (se *ScoringEngine) GetCredlists() (any, error) { a.Example = credlist.CredlistExplainText a.Path = credlist.CredlistPath a.Usernames = []string{} - file, err := safeOpenInDir("config/credlists", credlist.CredlistPath) - if err != nil { - return nil, fmt.Errorf("failed to open credlist file %s: %v", credlist.CredlistPath, err) - } - defer file.Close() - reader := csv.NewReader(file) - records, err := reader.ReadAll() + // Get usernames from original credentials in DB + origCreds, err := db.GetOriginalCredentials(credlist.CredlistPath) if err != nil { - return nil, fmt.Errorf("failed to read credlist file %s: %v", credlist.CredlistPath, err) + return nil, fmt.Errorf("failed to get original credentials for %s: %v", credlist.CredlistPath, err) } - for _, record := range records { - if len(record) != 2 { - return nil, fmt.Errorf("invalid credlist format") - } - a.Usernames = append(a.Usernames, record[0]) + for _, cred := range origCreds { + a.Usernames = append(a.Usernames, cred.Username) } credlists = append(credlists, a) } return credlists, nil } -func (se *ScoringEngine) ResetCredentials(teamID uint, credlistName string) error { - // check if the credlist name is in the config +func (se *ScoringEngine) ResetCredentials(teamID uint, credlistName string, changedBy string) error { + // Validate credlist name validCredlist := false - for _, c := range se.Config.CredlistSettings.Credlist { if c.CredlistPath == credlistName { validCredlist = true @@ -219,22 +209,15 @@ func (se *ScoringEngine) ResetCredentials(teamID uint, credlistName string) erro se.CredentialsMutex[teamID].Lock() defer se.CredentialsMutex[teamID].Unlock() - sourceFile, err := safeOpenInDir("config/credlists", credlistName) - if err != nil { - return fmt.Errorf("failed to open source file %s: %v", credlistName, err) - } - defer sourceFile.Close() - - destFile, err := safeCreateInDir(fmt.Sprintf("submissions/pcrs/%d", teamID), credlistName) - if err != nil { - return fmt.Errorf("failed to create destination file %s: %v", credlistName, err) - } - defer destFile.Close() + return db.ResetTeamCredlist(teamID, credlistName, changedBy) +} - _, err = io.Copy(destFile, sourceFile) - if err != nil { - return fmt.Errorf("failed to copy file: %v", err) - } +// GetTeamCredentials returns credentials for admin viewing +func (se *ScoringEngine) GetTeamCredentials(teamID uint, credlistName string) ([]db.CredentialSchema, error) { + return db.GetTeamCredentials(teamID, credlistName) +} - return nil +// GetPCRHistory returns PCR history for admin viewing +func (se *ScoringEngine) GetPCRHistory(teamID uint, credlistName string, username string) ([]db.PCRHistorySchema, error) { + return db.GetPCRHistory(teamID, credlistName, username) } diff --git a/engine/db/credentials.go b/engine/db/credentials.go new file mode 100644 index 00000000..5c16817f --- /dev/null +++ b/engine/db/credentials.go @@ -0,0 +1,206 @@ +package db + +import ( + "errors" + "time" + + "gorm.io/gorm" +) + +// OriginalCredentialSchema stores the original/default credentials for each credlist. +// Shared across all teams. Used as source of truth for resets. +type OriginalCredentialSchema struct { + ID uint `gorm:"primaryKey"` + CredlistName string `gorm:"uniqueIndex:idx_orig_credlist_user;not null"` + Username string `gorm:"uniqueIndex:idx_orig_credlist_user;not null"` + Password string `gorm:"not null"` +} + +// CredentialSchema stores current credential state per team. +// This is what the scoring engine uses for checks. +type CredentialSchema struct { + ID uint `gorm:"primaryKey"` + TeamID uint `gorm:"uniqueIndex:idx_team_credlist_user;not null"` + CredlistName string `gorm:"uniqueIndex:idx_team_credlist_user;not null"` + Username string `gorm:"uniqueIndex:idx_team_credlist_user;not null"` + Password string `gorm:"not null"` + UpdatedAt time.Time `gorm:"autoUpdateTime"` +} + +// PCRHistorySchema stores full audit trail of every credential change. +type PCRHistorySchema struct { + ID uint `gorm:"primaryKey"` + TeamID uint `gorm:"index;not null"` + CredlistName string `gorm:"not null"` + Username string `gorm:"not null"` + OldPassword string // empty for initial seeding + NewPassword string `gorm:"not null"` + ChangedBy string // username: "team3", "admin", or "system" + ChangedAt time.Time `gorm:"autoCreateTime"` +} + +// SeedOriginalCredentials inserts original credentials from config (called once on first startup) +func SeedOriginalCredentials(credlistName string, credentials [][]string) error { + for _, cred := range credentials { + if len(cred) != 2 { + continue + } + orig := OriginalCredentialSchema{ + CredlistName: credlistName, + Username: cred[0], + Password: cred[1], + } + result := db.Where("credlist_name = ? AND username = ?", credlistName, cred[0]).FirstOrCreate(&orig) + if result.Error != nil { + return result.Error + } + } + return nil +} + +// SeedTeamCredentials copies original credentials to a team's credential set +func SeedTeamCredentials(teamID uint) error { + var originals []OriginalCredentialSchema + if err := db.Find(&originals).Error; err != nil { + return err + } + for _, orig := range originals { + cred := CredentialSchema{ + TeamID: teamID, + CredlistName: orig.CredlistName, + Username: orig.Username, + Password: orig.Password, + } + result := db.Where("team_id = ? AND credlist_name = ? AND username = ?", teamID, orig.CredlistName, orig.Username).FirstOrCreate(&cred) + if result.Error != nil { + return result.Error + } + } + return nil +} + +// GetTeamCredentials returns all credentials for a team and credlist +func GetTeamCredentials(teamID uint, credlistName string) ([]CredentialSchema, error) { + var creds []CredentialSchema + result := db.Where("team_id = ? AND credlist_name = ?", teamID, credlistName).Find(&creds) + if result.Error != nil { + return nil, result.Error + } + return creds, nil +} + +// GetAllTeamCredentials returns all credentials for a team (all credlists) +func GetAllTeamCredentials(teamID uint) ([]CredentialSchema, error) { + var creds []CredentialSchema + result := db.Where("team_id = ?", teamID).Find(&creds) + if result.Error != nil { + return nil, result.Error + } + return creds, nil +} + +// UpdateCredential updates a single credential and logs to history +func UpdateCredential(teamID uint, credlistName, username, newPassword, changedBy string) error { + var cred CredentialSchema + result := db.Where("team_id = ? AND credlist_name = ? AND username = ?", teamID, credlistName, username).First(&cred) + if result.Error != nil { + if errors.Is(result.Error, gorm.ErrRecordNotFound) { + return errors.New("credential not found") + } + return result.Error + } + + oldPassword := cred.Password + + cred.Password = newPassword + if err := db.Save(&cred).Error; err != nil { + return err + } + + history := PCRHistorySchema{ + TeamID: teamID, + CredlistName: credlistName, + Username: username, + OldPassword: oldPassword, + NewPassword: newPassword, + ChangedBy: changedBy, + } + return db.Create(&history).Error +} + +// ResetTeamCredlist resets a team's credlist to original values +func ResetTeamCredlist(teamID uint, credlistName, changedBy string) error { + var currentCreds []CredentialSchema + if err := db.Where("team_id = ? AND credlist_name = ?", teamID, credlistName).Find(¤tCreds).Error; err != nil { + return err + } + + var originals []OriginalCredentialSchema + if err := db.Where("credlist_name = ?", credlistName).Find(&originals).Error; err != nil { + return err + } + + origMap := make(map[string]string) + for _, orig := range originals { + origMap[orig.Username] = orig.Password + } + + for _, cred := range currentCreds { + origPassword, exists := origMap[cred.Username] + if !exists { + continue + } + + history := PCRHistorySchema{ + TeamID: teamID, + CredlistName: credlistName, + Username: cred.Username, + OldPassword: cred.Password, + NewPassword: origPassword, + ChangedBy: changedBy, + } + if err := db.Create(&history).Error; err != nil { + return err + } + + cred.Password = origPassword + if err := db.Save(&cred).Error; err != nil { + return err + } + } + + return nil +} + +// GetPCRHistory returns history for a team/credlist, optionally filtered by username +func GetPCRHistory(teamID uint, credlistName string, username string) ([]PCRHistorySchema, error) { + var history []PCRHistorySchema + query := db.Where("team_id = ? AND credlist_name = ?", teamID, credlistName) + if username != "" { + query = query.Where("username = ?", username) + } + result := query.Order("changed_at DESC").Find(&history) + if result.Error != nil { + return nil, result.Error + } + return history, nil +} + +// IsCredentialsSeeded checks if original credentials have been seeded +func IsCredentialsSeeded() (bool, error) { + var count int64 + if err := db.Model(&OriginalCredentialSchema{}).Count(&count).Error; err != nil { + return false, err + } + return count > 0, nil +} + +// GetOriginalCredentials returns original credentials for a credlist +func GetOriginalCredentials(credlistName string) ([]OriginalCredentialSchema, error) { + var creds []OriginalCredentialSchema + result := db.Where("credlist_name = ?", credlistName).Find(&creds) + if result.Error != nil { + return nil, result.Error + } + return creds, nil +} diff --git a/engine/db/db.go b/engine/db/db.go index 0c2ebe1f..a8af334e 100644 --- a/engine/db/db.go +++ b/engine/db/db.go @@ -43,7 +43,9 @@ func Connect(connectURL string) { &TeamSchema{}, &RoundSchema{}, &ServiceCheckSchema{}, &SLASchema{}, &ManualAdjustmentSchema{}, &InjectSchema{}, &SubmissionSchema{}, &TeamServiceCheckSchema{}, // box schema must come first for automigrate to work - &VulnSchema{}, &BoxSchema{}, &VectorSchema{}, &AttackSchema{}, &CompetitionStateSchema{}) + &VulnSchema{}, &BoxSchema{}, &VectorSchema{}, &AttackSchema{}, &CompetitionStateSchema{}, + // credential schemas for PCR management + &OriginalCredentialSchema{}, &CredentialSchema{}, &PCRHistorySchema{}) if err != nil { log.Fatalln("Failed to auto migrate:", err) } diff --git a/engine/engine.go b/engine/engine.go index 21817bca..e4be4287 100644 --- a/engine/engine.go +++ b/engine/engine.go @@ -90,7 +90,7 @@ func (se *ScoringEngine) Start() { } // load credentials - err := se.LoadCredentials() + err := se.EnsureCredentialsSeeded() if err != nil { slog.Error("failed to load credential files into teams", "error", err) } @@ -417,6 +417,28 @@ func (se *ScoringEngine) rvb() error { CheckData: data, // the entire specialized struct } + // Populate credentials if check needs them + if credlists := r.GetCredlists(); len(credlists) > 0 { + for _, credlistName := range credlists { + creds, err := db.GetTeamCredentials(team.ID, credlistName) + if err != nil { + slog.Error("failed to get credentials for task", "team", team.ID, "credlist", credlistName, "error", err) + continue + } + for _, c := range creds { + task.Credentials = append(task.Credentials, Credential{ + Username: c.Username, + Password: c.Password, + }) + } + } + // Skip task if check requires credentials but none were loaded + if len(task.Credentials) == 0 { + slog.Error("skipping check: requires credentials but none seeded", "service", r.GetName(), "team", team.ID) + continue + } + } + payload, err := json.Marshal(task) if err != nil { slog.Error("failed to marshal service task", "error", err) diff --git a/engine/task.go b/engine/task.go index 5e8bfacc..2a03b585 100644 --- a/engine/task.go +++ b/engine/task.go @@ -5,6 +5,12 @@ import ( "time" ) +// Credential represents a username/password pair for task execution +type Credential struct { + Username string `json:"username"` + Password string `json:"password"` +} + type Task struct { TeamID uint `json:"team_id"` // Numeric identifier for the team TeamIdentifier string `json:"team_identifier"` // Human-readable identifier for the team @@ -14,4 +20,5 @@ type Task struct { RoundID uint `json:"round_id"` Attempts int `json:"attempts"` CheckData json.RawMessage `json:"check_data"` + Credentials []Credential `json:"credentials,omitempty"` } diff --git a/runner/runner.go b/runner/runner.go index 4fbc46d7..f036c8b6 100644 --- a/runner/runner.go +++ b/runner/runner.go @@ -179,6 +179,18 @@ func handleTask(ctx context.Context, rdb *redis.Client, runner checks.Runner, ta resultsChan := make(chan checks.Result, 1) + // Set credentials from task payload for the checks to use (per-instance, thread-safe) + if len(task.Credentials) > 0 { + creds := make([]checks.TaskCredential, len(task.Credentials)) + for i, c := range task.Credentials { + creds[i] = checks.TaskCredential{ + Username: c.Username, + Password: c.Password, + } + } + runner.SetTaskCredentials(creds) + } + // this currently discards all failed attempts for i := range task.Attempts { log.Printf("[Runner] Running check: RoundID=%d TeamID=%d ServiceType=%s ServiceName=%s Attempt=%d", diff --git a/static/templates/pages/pcr.html b/static/templates/pages/pcr.html index 2cfe3a09..6a00442d 100644 --- a/static/templates/pages/pcr.html +++ b/static/templates/pages/pcr.html @@ -1,286 +1,793 @@ {{ define "page" }} +
Update service credentials for your team's scored systems.
+