refactor(ax): round 7 AX sweep — replace banned imports with core primitives, add security tests
Some checks failed
Security Scan / security (push) Successful in 10s
Test / test (push) Failing after 35s

Replace fmt/strings/path/filepath with core.Sprintf, core.Lower, core.Upper, core.Split,
core.SplitN, core.Trim, core.Join, core.Contains, core.NewBuilder, core.Path across
ai/, cmd/metrics/, and cmd/security/. Add Bad/Ugly test coverage for the untested
security package (AlertSummary, filterBySeverity, buildTargetRepo, buildJobIssueBody).
os/exec kept for gh CLI invocations (no core equivalent in dep graph).
encoding/json kept for MarshalIndent (no core.JSONMarshalIndent yet).

Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
Snider 2026-03-31 14:40:57 +01:00
parent 45e76101bf
commit aff8ff4b3b
11 changed files with 360 additions and 91 deletions

View file

@ -5,11 +5,11 @@ import (
"cmp"
"encoding/json"
"os"
"path/filepath"
"slices"
"sync"
"time"
core "dappco.re/go/core"
coreio "dappco.re/go/core/io"
coreerr "dappco.re/go/core/log"
)
@ -36,14 +36,14 @@ func metricsDir() (string, error) {
if err != nil {
return "", coreerr.E("ai.metricsDir", "get home directory", err)
}
return filepath.Join(home, ".core", "ai", "metrics"), nil
return core.Path(home, ".core", "ai", "metrics"), nil
}
// metricsFilePath joins the daily JSONL filename onto dir.
//
// ai.metricsFilePath("/home/user/.core/ai/metrics", time.Now()) // → "/home/user/.core/ai/metrics/2026-03-31.jsonl"
func metricsFilePath(dir string, t time.Time) string {
return filepath.Join(dir, t.Format("2006-01-02")+".jsonl")
return core.Path(dir, t.Format("2006-01-02")+".jsonl")
}
// Record appends an event to ~/.core/ai/metrics/YYYY-MM-DD.jsonl.

View file

@ -1,12 +1,11 @@
package ai
import (
"fmt"
"os"
"path/filepath"
"testing"
"time"
core "dappco.re/go/core"
coreio "dappco.re/go/core/io"
)
@ -17,7 +16,7 @@ func setupBenchMetricsDir(b *testing.B) string {
b.Helper()
origHome := os.Getenv("HOME")
tmpHome := b.TempDir()
metricsPath := filepath.Join(tmpHome, ".core", "ai", "metrics")
metricsPath := core.Path(tmpHome, ".core", "ai", "metrics")
if err := coreio.Local.EnsureDir(metricsPath); err != nil {
b.Fatalf("Failed to create metrics dir: %v", err)
}
@ -36,10 +35,10 @@ func seedEvents(b *testing.B, count int) {
now := time.Now()
for eventIndex := range count {
event := Event{
Type: fmt.Sprintf("type-%d", eventIndex%10),
Type: core.Sprintf("type-%d", eventIndex%10),
Timestamp: now.Add(-time.Duration(eventIndex) * time.Millisecond),
AgentID: fmt.Sprintf("agent-%d", eventIndex%5),
Repo: fmt.Sprintf("repo-%d", eventIndex%3),
AgentID: core.Sprintf("agent-%d", eventIndex%5),
Repo: core.Sprintf("repo-%d", eventIndex%3),
Data: map[string]any{"index": eventIndex, "tool": "bench_tool"},
}
if err := Record(event); err != nil {
@ -161,7 +160,7 @@ func BenchmarkMetricsRecordAndQuery(b *testing.B) {
// Write 10K events
for eventIndex := range 10_000 {
event := Event{
Type: fmt.Sprintf("type-%d", eventIndex%10),
Type: core.Sprintf("type-%d", eventIndex%10),
Timestamp: now,
AgentID: "bench",
Repo: "bench-repo",
@ -192,10 +191,10 @@ func TestMetricsBench_RecordAndRead_10K_Good(t *testing.T) {
for eventIndex := range eventCount {
event := Event{
Type: fmt.Sprintf("type-%d", eventIndex%10),
Type: core.Sprintf("type-%d", eventIndex%10),
Timestamp: now.Add(-time.Duration(eventIndex) * time.Millisecond),
AgentID: fmt.Sprintf("agent-%d", eventIndex%5),
Repo: fmt.Sprintf("repo-%d", eventIndex%3),
AgentID: core.Sprintf("agent-%d", eventIndex%5),
Repo: core.Sprintf("repo-%d", eventIndex%3),
Data: map[string]any{"index": eventIndex},
}
if err := Record(event); err != nil {

View file

@ -2,10 +2,10 @@ package ai
import (
"os"
"path/filepath"
"testing"
"time"
core "dappco.re/go/core"
coreio "dappco.re/go/core/io"
)
@ -14,7 +14,7 @@ func withTempHome(t *testing.T) {
t.Helper()
origHome := os.Getenv("HOME")
tmpHome := t.TempDir()
metricsPath := filepath.Join(tmpHome, ".core", "ai", "metrics")
metricsPath := core.Path(tmpHome, ".core", "ai", "metrics")
if err := coreio.Local.EnsureDir(metricsPath); err != nil {
t.Fatalf("create metrics dir: %v", err)
}

View file

@ -3,11 +3,10 @@ package metrics
import (
"encoding/json"
"fmt"
"strconv"
"strings"
"time"
core "dappco.re/go/core"
"dappco.re/go/core/ai/ai"
"dappco.re/go/core/i18n"
coreerr "dappco.re/go/core/log"
@ -116,19 +115,19 @@ func runMetrics() error {
// parseDuration("30m") // → 30 * time.Minute
func parseDuration(durationText string) (time.Duration, error) {
if len(durationText) < 2 {
return 0, coreerr.E("metrics.parseDuration", fmt.Sprintf("invalid duration: %s", durationText), nil)
return 0, coreerr.E("metrics.parseDuration", core.Sprintf("invalid duration: %s", durationText), nil)
}
unitSuffix := durationText[len(durationText)-1]
countText := strings.TrimSpace(durationText[:len(durationText)-1])
countText := core.Trim(durationText[:len(durationText)-1])
quantity, err := strconv.Atoi(countText)
if err != nil {
return 0, coreerr.E("metrics.parseDuration", fmt.Sprintf("invalid duration: %s", durationText), nil)
return 0, coreerr.E("metrics.parseDuration", core.Sprintf("invalid duration: %s", durationText), nil)
}
if quantity <= 0 {
return 0, coreerr.E("metrics.parseDuration", fmt.Sprintf("duration must be positive: %s", durationText), nil)
return 0, coreerr.E("metrics.parseDuration", core.Sprintf("duration must be positive: %s", durationText), nil)
}
switch unitSuffix {
@ -139,6 +138,6 @@ func parseDuration(durationText string) (time.Duration, error) {
case 'm':
return time.Duration(quantity) * time.Minute, nil
default:
return 0, coreerr.E("metrics.parseDuration", fmt.Sprintf("unknown unit %c in duration: %s", unitSuffix, durationText), nil)
return 0, coreerr.E("metrics.parseDuration", core.Sprintf("unknown unit %c in duration: %s", unitSuffix, durationText), nil)
}
}

View file

@ -2,8 +2,8 @@ package security
import (
"encoding/json"
"fmt"
core "dappco.re/go/core"
"dappco.re/go/core/i18n"
"forge.lthn.ai/core/cli/pkg/cli"
)
@ -71,7 +71,7 @@ func runAlerts() error {
summary := &AlertSummary{}
for _, repo := range repoList {
repoFullName := fmt.Sprintf("%s/%s", registry.Org, repo.Name)
repoFullName := core.Sprintf("%s/%s", registry.Org, repo.Name)
dependabotAlerts, err := fetchDependabotAlerts(repoFullName)
if err == nil {
@ -107,7 +107,7 @@ func runAlerts() error {
continue
}
summary.Add(severity)
location := fmt.Sprintf("%s:%d", alert.MostRecentInstance.Location.Path, alert.MostRecentInstance.Location.StartLine)
location := core.Sprintf("%s:%d", alert.MostRecentInstance.Location.Path, alert.MostRecentInstance.Location.StartLine)
allAlerts = append(allAlerts, AlertOutput{
Repo: repo.Name,
Severity: severity,
@ -132,7 +132,7 @@ func runAlerts() error {
allAlerts = append(allAlerts, AlertOutput{
Repo: repo.Name,
Severity: "high",
ID: fmt.Sprintf("secret-%d", alert.Number),
ID: core.Sprintf("secret-%d", alert.Number),
Type: "secret-scanning",
Message: alert.SecretType,
})
@ -165,12 +165,12 @@ func runAlerts() error {
location = alert.Location
}
if alert.Version != "" {
location = fmt.Sprintf("%s %s", location, cli.DimStyle.Render(alert.Version))
location = core.Sprintf("%s %s", location, cli.DimStyle.Render(alert.Version))
}
cli.Print("%-20s %s %-16s %-40s %s\n",
cli.ValueStyle.Render(alert.Repo),
severityRenderer.Render(fmt.Sprintf("%-8s", alert.Severity)),
severityRenderer.Render(core.Sprintf("%-8s", alert.Severity)),
alert.ID,
location,
cli.DimStyle.Render(alert.Type),
@ -227,7 +227,7 @@ func runAlertsForTarget(target string) error {
continue
}
summary.Add(severity)
location := fmt.Sprintf("%s:%d", alert.MostRecentInstance.Location.Path, alert.MostRecentInstance.Location.StartLine)
location := core.Sprintf("%s:%d", alert.MostRecentInstance.Location.Path, alert.MostRecentInstance.Location.StartLine)
allAlerts = append(allAlerts, AlertOutput{
Repo: repo.Name,
Severity: severity,
@ -252,7 +252,7 @@ func runAlertsForTarget(target string) error {
allAlerts = append(allAlerts, AlertOutput{
Repo: repo.Name,
Severity: "high",
ID: fmt.Sprintf("secret-%d", alert.Number),
ID: core.Sprintf("secret-%d", alert.Number),
Type: "secret-scanning",
Message: alert.SecretType,
})
@ -283,11 +283,11 @@ func runAlertsForTarget(target string) error {
location = alert.Location
}
if alert.Version != "" {
location = fmt.Sprintf("%s %s", location, cli.DimStyle.Render(alert.Version))
location = core.Sprintf("%s %s", location, cli.DimStyle.Render(alert.Version))
}
cli.Print("%-20s %s %-16s %-40s %s\n",
cli.ValueStyle.Render(alert.Repo),
severityRenderer.Render(fmt.Sprintf("%-8s", alert.Severity)),
severityRenderer.Render(core.Sprintf("%-8s", alert.Severity)),
alert.ID,
location,
cli.DimStyle.Render(alert.Type),
@ -302,15 +302,15 @@ func runAlertsForTarget(target string) error {
//
// alerts, _ := fetchDependabotAlerts("host-uk/core-php")
func fetchDependabotAlerts(repoFullName string) ([]DependabotAlert, error) {
endpoint := fmt.Sprintf("repos/%s/dependabot/alerts?state=open", repoFullName)
endpoint := core.Sprintf("repos/%s/dependabot/alerts?state=open", repoFullName)
output, err := runGHAPI(endpoint)
if err != nil {
return nil, cli.Wrap(err, fmt.Sprintf("fetch dependabot alerts for %s", repoFullName))
return nil, cli.Wrap(err, core.Sprintf("fetch dependabot alerts for %s", repoFullName))
}
var alerts []DependabotAlert
if err := json.Unmarshal(output, &alerts); err != nil {
return nil, cli.Wrap(err, fmt.Sprintf("parse dependabot alerts for %s", repoFullName))
return nil, cli.Wrap(err, core.Sprintf("parse dependabot alerts for %s", repoFullName))
}
return alerts, nil
}
@ -319,15 +319,15 @@ func fetchDependabotAlerts(repoFullName string) ([]DependabotAlert, error) {
//
// alerts, _ := fetchCodeScanningAlerts("host-uk/core-php")
func fetchCodeScanningAlerts(repoFullName string) ([]CodeScanningAlert, error) {
endpoint := fmt.Sprintf("repos/%s/code-scanning/alerts?state=open", repoFullName)
endpoint := core.Sprintf("repos/%s/code-scanning/alerts?state=open", repoFullName)
output, err := runGHAPI(endpoint)
if err != nil {
return nil, cli.Wrap(err, fmt.Sprintf("fetch code-scanning alerts for %s", repoFullName))
return nil, cli.Wrap(err, core.Sprintf("fetch code-scanning alerts for %s", repoFullName))
}
var alerts []CodeScanningAlert
if err := json.Unmarshal(output, &alerts); err != nil {
return nil, cli.Wrap(err, fmt.Sprintf("parse code-scanning alerts for %s", repoFullName))
return nil, cli.Wrap(err, core.Sprintf("parse code-scanning alerts for %s", repoFullName))
}
return alerts, nil
}
@ -336,15 +336,15 @@ func fetchCodeScanningAlerts(repoFullName string) ([]CodeScanningAlert, error) {
//
// alerts, _ := fetchSecretScanningAlerts("host-uk/core-php")
func fetchSecretScanningAlerts(repoFullName string) ([]SecretScanningAlert, error) {
endpoint := fmt.Sprintf("repos/%s/secret-scanning/alerts?state=open", repoFullName)
endpoint := core.Sprintf("repos/%s/secret-scanning/alerts?state=open", repoFullName)
output, err := runGHAPI(endpoint)
if err != nil {
return nil, cli.Wrap(err, fmt.Sprintf("fetch secret-scanning alerts for %s", repoFullName))
return nil, cli.Wrap(err, core.Sprintf("fetch secret-scanning alerts for %s", repoFullName))
}
var alerts []SecretScanningAlert
if err := json.Unmarshal(output, &alerts); err != nil {
return nil, cli.Wrap(err, fmt.Sprintf("parse secret-scanning alerts for %s", repoFullName))
return nil, cli.Wrap(err, core.Sprintf("parse secret-scanning alerts for %s", repoFullName))
}
return alerts, nil
}

View file

@ -2,8 +2,8 @@ package security
import (
"encoding/json"
"fmt"
core "dappco.re/go/core"
"dappco.re/go/core/i18n"
"forge.lthn.ai/core/cli/pkg/cli"
)
@ -72,7 +72,7 @@ func runDeps() error {
summary := &AlertSummary{}
for _, repo := range repoList {
repoFullName := fmt.Sprintf("%s/%s", registry.Org, repo.Name)
repoFullName := core.Sprintf("%s/%s", registry.Org, repo.Name)
dependabotAlerts, err := fetchDependabotAlerts(repoFullName)
if err != nil {
@ -129,12 +129,12 @@ func runDeps() error {
upgrade := alert.Vulnerable
if alert.PatchedVersion != "" {
upgrade = fmt.Sprintf("%s -> %s", alert.Vulnerable, cli.SuccessStyle.Render(alert.PatchedVersion))
upgrade = core.Sprintf("%s -> %s", alert.Vulnerable, cli.SuccessStyle.Render(alert.PatchedVersion))
}
cli.Print("%-16s %s %-16s %-30s %s\n",
cli.ValueStyle.Render(alert.Repo),
severityRenderer.Render(fmt.Sprintf("%-8s", alert.Severity)),
severityRenderer.Render(core.Sprintf("%-8s", alert.Severity)),
alert.CVE,
alert.Package,
upgrade,
@ -201,11 +201,11 @@ func runDepsForTarget(target string) error {
severityRenderer := severityStyle(alert.Severity)
upgrade := alert.Vulnerable
if alert.PatchedVersion != "" {
upgrade = fmt.Sprintf("%s -> %s", alert.Vulnerable, cli.SuccessStyle.Render(alert.PatchedVersion))
upgrade = core.Sprintf("%s -> %s", alert.Vulnerable, cli.SuccessStyle.Render(alert.PatchedVersion))
}
cli.Print("%-16s %s %-16s %-30s %s\n",
cli.ValueStyle.Render(alert.Repo),
severityRenderer.Render(fmt.Sprintf("%-8s", alert.Severity)),
severityRenderer.Render(core.Sprintf("%-8s", alert.Severity)),
alert.CVE,
alert.Package,
upgrade,

View file

@ -1,11 +1,10 @@
package security
import (
"fmt"
"os/exec"
"strings"
"time"
core "dappco.re/go/core"
"dappco.re/go/core/ai/ai"
"dappco.re/go/core/i18n"
coreerr "dappco.re/go/core/log"
@ -77,7 +76,7 @@ func runJobs() error {
//
// createJobForTarget("wailsapp/wails")
func createJobForTarget(target string) error {
targetParts := strings.SplitN(target, "/", 2)
targetParts := core.SplitN(target, "/", 2)
if len(targetParts) != 2 || targetParts[0] == "" || targetParts[1] == "" {
return coreerr.E("security.createJobForTarget", "invalid target format: use owner/repo", nil)
}
@ -101,8 +100,8 @@ func createJobForTarget(target string) error {
severity = "medium"
}
summary.Add(severity)
findings = append(findings, fmt.Sprintf("- [%s] %s: %s (%s:%d)",
strings.ToUpper(severity), alert.Tool.Name, alert.Rule.Description,
findings = append(findings, core.Sprintf("- [%s] %s: %s (%s:%d)",
core.Upper(severity), alert.Tool.Name, alert.Rule.Description,
alert.MostRecentInstance.Location.Path, alert.MostRecentInstance.Location.StartLine))
}
}
@ -118,8 +117,8 @@ func createJobForTarget(target string) error {
continue
}
summary.Add(alert.Advisory.Severity)
findings = append(findings, fmt.Sprintf("- [%s] %s: %s (%s)",
strings.ToUpper(alert.Advisory.Severity), alert.Dependency.Package.Name,
findings = append(findings, core.Sprintf("- [%s] %s: %s (%s)",
core.Upper(alert.Advisory.Severity), alert.Dependency.Package.Name,
alert.Advisory.Summary, alert.Advisory.CVEID))
}
}
@ -135,12 +134,12 @@ func createJobForTarget(target string) error {
continue
}
summary.Add("high")
findings = append(findings, fmt.Sprintf("- [HIGH] Secret: %s (#%d)", alert.SecretType, alert.Number))
findings = append(findings, core.Sprintf("- [HIGH] Secret: %s (#%d)", alert.SecretType, alert.Number))
}
}
if fetchErrors == 3 {
return coreerr.E("security.createJobForTarget", fmt.Sprintf("failed to fetch any alerts for %s", target), nil)
return coreerr.E("security.createJobForTarget", core.Sprintf("failed to fetch any alerts for %s", target), nil)
}
if summary.Total == 0 {
@ -148,13 +147,13 @@ func createJobForTarget(target string) error {
return nil
}
title := fmt.Sprintf("Security scan: %s", target)
title := core.Sprintf("Security scan: %s", target)
issueBody := buildJobIssueBody(target, summary, findings)
for copyIndex := range jobsCopies {
issueTitle := title
if jobsCopies > 1 {
issueTitle = fmt.Sprintf("%s (#%d)", title, copyIndex+1)
issueTitle = core.Sprintf("%s (#%d)", title, copyIndex+1)
}
if jobsDryRun {
@ -175,10 +174,10 @@ func createJobForTarget(target string) error {
output, err := issueCommand.CombinedOutput()
if err != nil {
return cli.Wrap(err, fmt.Sprintf("create issue for %s: %s", target, string(output)))
return cli.Wrap(err, core.Sprintf("create issue for %s: %s", target, string(output)))
}
issueURL := strings.TrimSpace(string(output))
issueURL := core.Trim(string(output))
cli.Print("%s %s: %s\n", cli.SuccessStyle.Render(">>"), issueTitle, issueURL)
_ = ai.Record(ai.Event{
@ -202,17 +201,17 @@ func createJobForTarget(target string) error {
//
// body := buildJobIssueBody("wailsapp/wails", summary, findings)
func buildJobIssueBody(target string, summary *AlertSummary, findings []string) string {
var issueBodyBuilder strings.Builder
issueBodyBuilder := core.NewBuilder()
fmt.Fprintf(&issueBodyBuilder, "## Security Scan: %s\n\n", target)
fmt.Fprintf(&issueBodyBuilder, "**Summary:** %s\n\n", summary.String())
issueBodyBuilder.WriteString(core.Sprintf("## Security Scan: %s\n\n", target))
issueBodyBuilder.WriteString(core.Sprintf("**Summary:** %s\n\n", summary.String()))
issueBodyBuilder.WriteString("### Findings\n\n")
if len(findings) > 50 {
for _, finding := range findings[:50] {
issueBodyBuilder.WriteString(finding + "\n")
}
fmt.Fprintf(&issueBodyBuilder, "\n... and %d more\n", len(findings)-50)
issueBodyBuilder.WriteString(core.Sprintf("\n... and %d more\n", len(findings)-50))
} else {
for _, finding := range findings {
issueBodyBuilder.WriteString(finding + "\n")
@ -227,7 +226,7 @@ func buildJobIssueBody(target string, summary *AlertSummary, findings []string)
issueBodyBuilder.WriteString("\n### Instructions\n\n")
issueBodyBuilder.WriteString("1. Claim this issue by assigning yourself\n")
fmt.Fprintf(&issueBodyBuilder, "2. Run `core security alerts --target %s` for the latest findings\n", target)
issueBodyBuilder.WriteString(core.Sprintf("2. Run `core security alerts --target %s` for the latest findings\n", target))
issueBodyBuilder.WriteString("3. Work through the checklist above\n")
issueBodyBuilder.WriteString("4. Close this issue when all findings are addressed\n")

View file

@ -2,9 +2,9 @@ package security
import (
"encoding/json"
"fmt"
"time"
core "dappco.re/go/core"
"dappco.re/go/core/ai/ai"
"dappco.re/go/core/i18n"
"forge.lthn.ai/core/cli/pkg/cli"
@ -78,7 +78,7 @@ func runScan() error {
summary := &AlertSummary{}
for _, repo := range repoList {
repoFullName := fmt.Sprintf("%s/%s", registry.Org, repo.Name)
repoFullName := core.Sprintf("%s/%s", registry.Org, repo.Name)
codeScanningAlerts, err := fetchCodeScanningAlerts(repoFullName)
if err != nil {
@ -152,11 +152,11 @@ func runScan() error {
for _, alert := range allAlerts {
severityRenderer := severityStyle(alert.Severity)
location := fmt.Sprintf("%s:%d", alert.Path, alert.Line)
location := core.Sprintf("%s:%d", alert.Path, alert.Line)
cli.Print("%-16s %s %-20s %-40s %s\n",
cli.ValueStyle.Render(alert.Repo),
severityRenderer.Render(fmt.Sprintf("%-8s", alert.Severity)),
severityRenderer.Render(core.Sprintf("%-8s", alert.Severity)),
alert.RuleID,
location,
cli.DimStyle.Render(alert.Tool),
@ -244,10 +244,10 @@ func runScanForTarget(target string) error {
for _, alert := range allAlerts {
severityRenderer := severityStyle(alert.Severity)
location := fmt.Sprintf("%s:%d", alert.Path, alert.Line)
location := core.Sprintf("%s:%d", alert.Path, alert.Line)
cli.Print("%-16s %s %-20s %-40s %s\n",
cli.ValueStyle.Render(alert.Repo),
severityRenderer.Render(fmt.Sprintf("%-8s", alert.Severity)),
severityRenderer.Render(core.Sprintf("%-8s", alert.Severity)),
alert.RuleID,
location,
cli.DimStyle.Render(alert.Tool),

View file

@ -2,8 +2,8 @@ package security
import (
"encoding/json"
"fmt"
core "dappco.re/go/core"
"dappco.re/go/core/i18n"
"forge.lthn.ai/core/cli/pkg/cli"
)
@ -68,7 +68,7 @@ func runSecrets() error {
openCount := 0
for _, repo := range repoList {
repoFullName := fmt.Sprintf("%s/%s", registry.Org, repo.Name)
repoFullName := core.Sprintf("%s/%s", registry.Org, repo.Name)
secretScanningAlerts, err := fetchSecretScanningAlerts(repoFullName)
if err != nil {
@ -103,7 +103,7 @@ func runSecrets() error {
cli.Blank()
if openCount > 0 {
cli.Print("%s %s\n", cli.DimStyle.Render("Secrets:"), cli.ErrorStyle.Render(fmt.Sprintf("%d open", openCount)))
cli.Print("%s %s\n", cli.DimStyle.Render("Secrets:"), cli.ErrorStyle.Render(core.Sprintf("%d open", openCount)))
} else {
cli.Print("%s %s\n", cli.DimStyle.Render("Secrets:"), cli.SuccessStyle.Render("No exposed secrets"))
}
@ -174,7 +174,7 @@ func runSecretsForTarget(target string) error {
cli.Blank()
if openCount > 0 {
cli.Print("%s %s\n", cli.DimStyle.Render("Secrets ("+fullName+"):"), cli.ErrorStyle.Render(fmt.Sprintf("%d open", openCount)))
cli.Print("%s %s\n", cli.DimStyle.Render("Secrets ("+fullName+"):"), cli.ErrorStyle.Render(core.Sprintf("%d open", openCount)))
} else {
cli.Print("%s %s\n", cli.DimStyle.Render("Secrets ("+fullName+"):"), cli.SuccessStyle.Render("No exposed secrets"))
}

View file

@ -1,11 +1,10 @@
package security
import (
"fmt"
"os/exec"
"slices"
"strings"
core "dappco.re/go/core"
"dappco.re/go/core/i18n"
"dappco.re/go/core/io"
coreerr "dappco.re/go/core/log"
@ -155,10 +154,10 @@ func runGHAPI(endpoint string) ([]byte, error) {
if err != nil {
if exitErr, ok := err.(*exec.ExitError); ok {
stderr := string(exitErr.Stderr)
if strings.Contains(stderr, "404") || strings.Contains(stderr, "Not Found") {
if core.Contains(stderr, "404") || core.Contains(stderr, "Not Found") {
return []byte("[]"), nil
}
if strings.Contains(stderr, "403") {
if core.Contains(stderr, "403") {
return nil, coreerr.E("security.runGHAPI", "access denied (check token permissions)", nil)
}
}
@ -172,7 +171,7 @@ func runGHAPI(endpoint string) ([]byte, error) {
// severityStyle("critical") // → cli.ErrorStyle
// severityStyle("high") // → cli.WarningStyle
func severityStyle(severity string) *cli.AnsiStyle {
switch strings.ToLower(severity) {
switch core.Lower(severity) {
case "critical":
return cli.ErrorStyle
case "high":
@ -194,9 +193,9 @@ func filterBySeverity(severity, filter string) bool {
return true
}
severityLower := strings.ToLower(severity)
return slices.ContainsFunc(slices.Collect(strings.SplitSeq(strings.ToLower(filter), ",")), func(filterEntry string) bool {
return strings.TrimSpace(filterEntry) == severityLower
severityLower := core.Lower(severity)
return slices.ContainsFunc(core.Split(core.Lower(filter), ","), func(filterEntry string) bool {
return core.Trim(filterEntry) == severityLower
})
}
@ -218,7 +217,7 @@ func getReposToCheck(registry *repos.Registry, repoFilter string) []*repos.Repo
//
// buildTargetRepo("wailsapp/wails") // → &Repo{Name:"wails"}, "wailsapp/wails"
func buildTargetRepo(target string) (*repos.Repo, string) {
targetParts := strings.SplitN(target, "/", 2)
targetParts := core.SplitN(target, "/", 2)
if len(targetParts) != 2 || targetParts[0] == "" || targetParts[1] == "" {
return nil, ""
}
@ -244,7 +243,7 @@ type AlertSummary struct {
// summary.Add("critical") // summary.Critical == 1, summary.Total == 1
func (summary *AlertSummary) Add(severity string) {
summary.Total++
switch strings.ToLower(severity) {
switch core.Lower(severity) {
case "critical":
summary.Critical++
case "high":
@ -264,22 +263,22 @@ func (summary *AlertSummary) Add(severity string) {
func (summary *AlertSummary) String() string {
segments := []string{}
if summary.Critical > 0 {
segments = append(segments, cli.ErrorStyle.Render(fmt.Sprintf("%d critical", summary.Critical)))
segments = append(segments, cli.ErrorStyle.Render(core.Sprintf("%d critical", summary.Critical)))
}
if summary.High > 0 {
segments = append(segments, cli.WarningStyle.Render(fmt.Sprintf("%d high", summary.High)))
segments = append(segments, cli.WarningStyle.Render(core.Sprintf("%d high", summary.High)))
}
if summary.Medium > 0 {
segments = append(segments, cli.ValueStyle.Render(fmt.Sprintf("%d medium", summary.Medium)))
segments = append(segments, cli.ValueStyle.Render(core.Sprintf("%d medium", summary.Medium)))
}
if summary.Low > 0 {
segments = append(segments, cli.DimStyle.Render(fmt.Sprintf("%d low", summary.Low)))
segments = append(segments, cli.DimStyle.Render(core.Sprintf("%d low", summary.Low)))
}
if summary.Unknown > 0 {
segments = append(segments, cli.DimStyle.Render(fmt.Sprintf("%d unknown", summary.Unknown)))
segments = append(segments, cli.DimStyle.Render(core.Sprintf("%d unknown", summary.Unknown)))
}
if len(segments) == 0 {
return cli.SuccessStyle.Render("No alerts")
}
return strings.Join(segments, " | ")
return core.Join(" | ", segments...)
}

View file

@ -0,0 +1,273 @@
package security
import (
"testing"
core "dappco.re/go/core"
)
// TestAlertSummary_Add_Good verifies that known severity levels increment the correct counter.
func TestAlertSummary_Add_Good(t *testing.T) {
summary := &AlertSummary{}
summary.Add("critical")
summary.Add("high")
summary.Add("medium")
summary.Add("low")
if summary.Critical != 1 {
t.Errorf("Critical: want 1, got %d", summary.Critical)
}
if summary.High != 1 {
t.Errorf("High: want 1, got %d", summary.High)
}
if summary.Medium != 1 {
t.Errorf("Medium: want 1, got %d", summary.Medium)
}
if summary.Low != 1 {
t.Errorf("Low: want 1, got %d", summary.Low)
}
if summary.Total != 4 {
t.Errorf("Total: want 4, got %d", summary.Total)
}
}
// TestAlertSummary_Add_Bad verifies that an unrecognised severity goes to the Unknown bucket.
func TestAlertSummary_Add_Bad(t *testing.T) {
summary := &AlertSummary{}
summary.Add("informational")
summary.Add("WEIRD")
if summary.Unknown != 2 {
t.Errorf("Unknown: want 2, got %d", summary.Unknown)
}
if summary.Total != 2 {
t.Errorf("Total: want 2, got %d", summary.Total)
}
if summary.Critical != 0 || summary.High != 0 {
t.Errorf("Critical/High should remain 0 for unknown severities")
}
}
// TestAlertSummary_Add_Ugly verifies case-insensitive matching — "CRITICAL" and "Critical" both hit Critical.
func TestAlertSummary_Add_Ugly(t *testing.T) {
summary := &AlertSummary{}
summary.Add("CRITICAL")
summary.Add("Critical")
summary.Add("HIGH")
summary.Add("Medium")
summary.Add("LOW")
if summary.Critical != 2 {
t.Errorf("Critical: want 2, got %d", summary.Critical)
}
if summary.High != 1 {
t.Errorf("High: want 1, got %d", summary.High)
}
if summary.Medium != 1 {
t.Errorf("Medium: want 1, got %d", summary.Medium)
}
if summary.Low != 1 {
t.Errorf("Low: want 1, got %d", summary.Low)
}
if summary.Total != 5 {
t.Errorf("Total: want 5, got %d", summary.Total)
}
}
// TestAlertSummaryString_Good verifies that String() renders all non-zero counts.
func TestAlertSummaryString_Good(t *testing.T) {
summary := &AlertSummary{Critical: 1, High: 2, Total: 3}
result := summary.String()
if !core.Contains(result, "1 critical") {
t.Errorf("expected '1 critical' in %q", result)
}
if !core.Contains(result, "2 high") {
t.Errorf("expected '2 high' in %q", result)
}
if !core.Contains(result, "|") {
t.Errorf("expected separator '|' between segments in %q", result)
}
}
// TestAlertSummaryString_Bad verifies that an empty AlertSummary returns the "No alerts" message.
func TestAlertSummaryString_Bad(t *testing.T) {
summary := &AlertSummary{}
result := summary.String()
if !core.Contains(result, "No alerts") {
t.Errorf("expected 'No alerts' for empty summary, got %q", result)
}
}
// TestAlertSummaryString_Ugly verifies that zero-count severity levels are omitted from the string.
func TestAlertSummaryString_Ugly(t *testing.T) {
// Only medium is set — critical/high/low/unknown should not appear.
summary := &AlertSummary{Medium: 3, Total: 3}
result := summary.String()
if core.Contains(result, "critical") {
t.Errorf("'critical' should be absent when count is 0, got %q", result)
}
if core.Contains(result, "high") {
t.Errorf("'high' should be absent when count is 0, got %q", result)
}
if !core.Contains(result, "3 medium") {
t.Errorf("expected '3 medium' in %q", result)
}
}
// TestFilterBySeverity_Good verifies that matching entries pass the filter.
func TestFilterBySeverity_Good(t *testing.T) {
cases := []struct {
severity string
filter string
}{
{"critical", "critical,high"},
{"high", "critical,high"},
{"medium", "medium"},
{"low", "low,medium,high"},
// Empty filter passes everything.
{"critical", ""},
{"anything", ""},
}
for _, tc := range cases {
if !filterBySeverity(tc.severity, tc.filter) {
t.Errorf("filterBySeverity(%q, %q): want true, got false", tc.severity, tc.filter)
}
}
}
// TestFilterBySeverity_Bad verifies that non-matching severities are excluded.
func TestFilterBySeverity_Bad(t *testing.T) {
cases := []struct {
severity string
filter string
}{
{"low", "critical,high"},
{"medium", "critical"},
{"informational", "critical,high,medium,low"},
}
for _, tc := range cases {
if filterBySeverity(tc.severity, tc.filter) {
t.Errorf("filterBySeverity(%q, %q): want false, got true", tc.severity, tc.filter)
}
}
}
// TestFilterBySeverity_Ugly verifies case-insensitive matching and whitespace tolerance.
func TestFilterBySeverity_Ugly(t *testing.T) {
// Severity and filter are uppercased — must still match.
if !filterBySeverity("CRITICAL", "CRITICAL,HIGH") {
t.Error("filterBySeverity should be case-insensitive for severity")
}
if !filterBySeverity("critical", "CRITICAL") {
t.Error("filterBySeverity should be case-insensitive for filter")
}
// Filter entries may have leading/trailing whitespace.
if !filterBySeverity("high", " high , critical ") {
t.Error("filterBySeverity should trim whitespace from filter entries")
}
}
// TestBuildTargetRepo_Good verifies that a valid owner/repo string parses correctly.
func TestBuildTargetRepo_Good(t *testing.T) {
repo, fullName := buildTargetRepo("wailsapp/wails")
if repo == nil {
t.Fatal("expected non-nil repo")
}
if repo.Name != "wails" {
t.Errorf("Name: want 'wails', got %q", repo.Name)
}
if fullName != "wailsapp/wails" {
t.Errorf("fullName: want 'wailsapp/wails', got %q", fullName)
}
}
// TestBuildTargetRepo_Bad verifies that malformed targets return nil.
func TestBuildTargetRepo_Bad(t *testing.T) {
badTargets := []string{
"", // empty
"noslash", // missing separator
"/repo", // empty owner
"owner/", // empty repo
"//double", // double slash
}
for _, target := range badTargets {
repo, fullName := buildTargetRepo(target)
if repo != nil || fullName != "" {
t.Errorf("buildTargetRepo(%q): expected (nil, ''), got (%v, %q)", target, repo, fullName)
}
}
}
// TestBuildTargetRepo_Ugly verifies targets with extra path segments — only the first two parts matter.
func TestBuildTargetRepo_Ugly(t *testing.T) {
// SplitN(n=2) means "owner/repo/extra" → ["owner", "repo/extra"]
// repo name becomes "repo/extra" which is non-empty, so it parses.
repo, fullName := buildTargetRepo("owner/repo/extra")
if repo == nil {
t.Fatal("expected non-nil repo for 3-segment path")
}
if repo.Name != "repo/extra" {
t.Errorf("Name: want 'repo/extra', got %q", repo.Name)
}
if fullName != "owner/repo/extra" {
t.Errorf("fullName: want 'owner/repo/extra', got %q", fullName)
}
}
// TestBuildJobIssueBody_Good verifies the issue body contains all required sections.
func TestBuildJobIssueBody_Good(t *testing.T) {
summary := &AlertSummary{Critical: 1, High: 2, Total: 3}
findings := []string{"- [CRITICAL] tool: description (file.go:10)"}
body := buildJobIssueBody("wailsapp/wails", summary, findings)
requiredSections := []string{
"## Security Scan: wailsapp/wails",
"### Findings",
"### Checklist",
"### Instructions",
"wailsapp/wails",
}
for _, section := range requiredSections {
if !core.Contains(body, section) {
t.Errorf("body missing section %q", section)
}
}
}
// TestBuildJobIssueBody_Bad verifies that more than 50 findings are truncated with a count line.
func TestBuildJobIssueBody_Bad(t *testing.T) {
summary := &AlertSummary{Total: 60}
findings := make([]string, 60)
for i := range 60 {
findings[i] = "- [HIGH] finding"
}
body := buildJobIssueBody("org/repo", summary, findings)
if !core.Contains(body, "... and 10 more") {
t.Errorf("expected truncation message '... and 10 more' in body, got:\n%s", body)
}
}
// TestBuildJobIssueBody_Ugly verifies that empty findings still produces a valid body with all sections.
func TestBuildJobIssueBody_Ugly(t *testing.T) {
summary := &AlertSummary{}
body := buildJobIssueBody("org/empty-repo", summary, nil)
for _, section := range []string{"### Findings", "### Checklist", "### Instructions"} {
if !core.Contains(body, section) {
t.Errorf("empty findings body missing section %q", section)
}
}
}