Compare commits
No commits in common. "dev" and "agent/read---spec-code-core-lint-rfc-md-fully" have entirely different histories.
dev
...
agent/read
26 changed files with 60 additions and 1051 deletions
BIN
.DS_Store
vendored
BIN
.DS_Store
vendored
Binary file not shown.
|
|
@ -9,10 +9,10 @@ import (
|
||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
cataloglint "dappco.re/go/core/lint"
|
|
||||||
lintpkg "dappco.re/go/core/lint/pkg/lint"
|
|
||||||
"forge.lthn.ai/core/cli/pkg/cli"
|
"forge.lthn.ai/core/cli/pkg/cli"
|
||||||
coreerr "forge.lthn.ai/core/go-log"
|
coreerr "forge.lthn.ai/core/go-log"
|
||||||
|
cataloglint "forge.lthn.ai/core/lint"
|
||||||
|
lintpkg "forge.lthn.ai/core/lint/pkg/lint"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
|
@ -265,18 +265,14 @@ func newCheckCommand() *cli.Command {
|
||||||
if language != "" {
|
if language != "" {
|
||||||
rules = catalog.ForLanguage(language)
|
rules = catalog.ForLanguage(language)
|
||||||
if len(rules) == 0 {
|
if len(rules) == 0 {
|
||||||
if _, err := fmt.Fprintf(os.Stderr, "no rules for language %q\n", language); err != nil {
|
fmt.Fprintf(os.Stderr, "no rules for language %q\n", language)
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if severity != "" {
|
if severity != "" {
|
||||||
filtered := (&lintpkg.Catalog{Rules: rules}).AtSeverity(severity)
|
filtered := (&lintpkg.Catalog{Rules: rules}).AtSeverity(severity)
|
||||||
if len(filtered) == 0 {
|
if len(filtered) == 0 {
|
||||||
if _, err := fmt.Fprintf(os.Stderr, "no rules at severity %q or above\n", severity); err != nil {
|
fmt.Fprintf(os.Stderr, "no rules at severity %q or above\n", severity)
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
rules = filtered
|
rules = filtered
|
||||||
|
|
@ -327,13 +323,9 @@ func newCheckCommand() *cli.Command {
|
||||||
}
|
}
|
||||||
return lintpkg.WriteReportSARIF(command.OutOrStdout(), report)
|
return lintpkg.WriteReportSARIF(command.OutOrStdout(), report)
|
||||||
default:
|
default:
|
||||||
if err := lintpkg.WriteText(command.OutOrStdout(), findings); err != nil {
|
lintpkg.WriteText(command.OutOrStdout(), findings)
|
||||||
return err
|
|
||||||
}
|
|
||||||
if format == "text" && len(findings) > 0 {
|
if format == "text" && len(findings) > 0 {
|
||||||
if err := writeCatalogSummary(command.OutOrStdout(), findings); err != nil {
|
writeCatalogSummary(command.OutOrStdout(), findings)
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
@ -376,9 +368,7 @@ func newCatalogCommand() *cli.Command {
|
||||||
for _, rule := range rules {
|
for _, rule := range rules {
|
||||||
fmt.Fprintf(command.OutOrStdout(), "%-14s [%-8s] %s\n", rule.ID, rule.Severity, rule.Title)
|
fmt.Fprintf(command.OutOrStdout(), "%-14s [%-8s] %s\n", rule.ID, rule.Severity, rule.Title)
|
||||||
}
|
}
|
||||||
if _, err := fmt.Fprintf(os.Stderr, "\n%d rule(s)\n", len(rules)); err != nil {
|
fmt.Fprintf(os.Stderr, "\n%d rule(s)\n", len(rules))
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
cli.StringFlag(listCmd, &listLanguage, "lang", "l", "", "Filter by language")
|
cli.StringFlag(listCmd, &listLanguage, "lang", "l", "", "Filter by language")
|
||||||
|
|
@ -415,9 +405,11 @@ func writeReport(writer io.Writer, output string, report lintpkg.Report) error {
|
||||||
case "json":
|
case "json":
|
||||||
return lintpkg.WriteReportJSON(writer, report)
|
return lintpkg.WriteReportJSON(writer, report)
|
||||||
case "text":
|
case "text":
|
||||||
return lintpkg.WriteReportText(writer, report)
|
lintpkg.WriteReportText(writer, report)
|
||||||
|
return nil
|
||||||
case "github":
|
case "github":
|
||||||
return lintpkg.WriteReportGitHub(writer, report)
|
lintpkg.WriteReportGitHub(writer, report)
|
||||||
|
return nil
|
||||||
case "sarif":
|
case "sarif":
|
||||||
return lintpkg.WriteReportSARIF(writer, report)
|
return lintpkg.WriteReportSARIF(writer, report)
|
||||||
default:
|
default:
|
||||||
|
|
@ -431,11 +423,9 @@ func writeIndentedJSON(writer io.Writer, value any) error {
|
||||||
return encoder.Encode(value)
|
return encoder.Encode(value)
|
||||||
}
|
}
|
||||||
|
|
||||||
func writeCatalogSummary(writer io.Writer, findings []lintpkg.Finding) error {
|
func writeCatalogSummary(writer io.Writer, findings []lintpkg.Finding) {
|
||||||
summary := lintpkg.Summarise(findings)
|
summary := lintpkg.Summarise(findings)
|
||||||
if _, err := fmt.Fprintf(writer, "\n%d finding(s)", summary.Total); err != nil {
|
fmt.Fprintf(writer, "\n%d finding(s)", summary.Total)
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
orderedSeverities := []string{"critical", "high", "medium", "low", "info", "error", "warning"}
|
orderedSeverities := []string{"critical", "high", "medium", "low", "info", "error", "warning"}
|
||||||
seen := make(map[string]bool, len(summary.BySeverity))
|
seen := make(map[string]bool, len(summary.BySeverity))
|
||||||
|
|
@ -467,12 +457,7 @@ func writeCatalogSummary(writer io.Writer, findings []lintpkg.Finding) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(parts) > 0 {
|
if len(parts) > 0 {
|
||||||
if _, err := fmt.Fprintf(writer, " (%s)", strings.Join(parts, ", ")); err != nil {
|
fmt.Fprintf(writer, " (%s)", strings.Join(parts, ", "))
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
}
|
fmt.Fprintln(writer)
|
||||||
if _, err := fmt.Fprintln(writer); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ import (
|
||||||
"sync"
|
"sync"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
lintpkg "dappco.re/go/core/lint/pkg/lint"
|
lintpkg "forge.lthn.ai/core/lint/pkg/lint"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
@ -44,24 +44,9 @@ func Run() {
|
||||||
|
|
||||||
var report lintpkg.Report
|
var report lintpkg.Report
|
||||||
require.NoError(t, json.Unmarshal([]byte(stdout), &report))
|
require.NoError(t, json.Unmarshal([]byte(stdout), &report))
|
||||||
require.NotEmpty(t, report.Findings)
|
require.Len(t, report.Findings, 1)
|
||||||
assert.GreaterOrEqual(t, report.Summary.Total, 2)
|
assert.Equal(t, "go-cor-003", report.Findings[0].Code)
|
||||||
assert.Greater(t, report.Summary.Info, 0)
|
assert.Equal(t, 1, report.Summary.Total)
|
||||||
assert.Contains(t, report.Summary.BySeverity, "info")
|
|
||||||
assert.Contains(t, report.Summary.BySeverity, "warning")
|
|
||||||
|
|
||||||
var hasCatalogFinding bool
|
|
||||||
var hasMissingToolFinding bool
|
|
||||||
for _, finding := range report.Findings {
|
|
||||||
switch finding.Code {
|
|
||||||
case "go-cor-003":
|
|
||||||
hasCatalogFinding = true
|
|
||||||
case "missing-tool":
|
|
||||||
hasMissingToolFinding = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
assert.True(t, hasCatalogFinding)
|
|
||||||
assert.True(t, hasMissingToolFinding)
|
|
||||||
assert.False(t, report.Summary.Passed)
|
assert.False(t, report.Summary.Passed)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -89,13 +74,8 @@ func helper() error { return nil }
|
||||||
|
|
||||||
var report lintpkg.Report
|
var report lintpkg.Report
|
||||||
require.NoError(t, json.Unmarshal([]byte(stdout), &report))
|
require.NoError(t, json.Unmarshal([]byte(stdout), &report))
|
||||||
require.NotEmpty(t, report.Findings)
|
assert.Empty(t, report.Findings)
|
||||||
assert.Equal(t, 6, report.Summary.Total)
|
assert.Equal(t, 0, report.Summary.Total)
|
||||||
assert.Equal(t, 6, report.Summary.Info)
|
|
||||||
for _, finding := range report.Findings {
|
|
||||||
assert.Equal(t, "missing-tool", finding.Code)
|
|
||||||
assert.Equal(t, "info", finding.Severity)
|
|
||||||
}
|
|
||||||
assert.True(t, report.Summary.Passed)
|
assert.True(t, report.Summary.Passed)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -135,13 +115,8 @@ schedules:
|
||||||
|
|
||||||
var report lintpkg.Report
|
var report lintpkg.Report
|
||||||
require.NoError(t, json.Unmarshal([]byte(stdout), &report))
|
require.NoError(t, json.Unmarshal([]byte(stdout), &report))
|
||||||
require.NotEmpty(t, report.Findings)
|
assert.Empty(t, report.Findings)
|
||||||
assert.Equal(t, 6, report.Summary.Total)
|
assert.Equal(t, 0, report.Summary.Total)
|
||||||
assert.Equal(t, 6, report.Summary.Info)
|
|
||||||
for _, finding := range report.Findings {
|
|
||||||
assert.Equal(t, "missing-tool", finding.Code)
|
|
||||||
assert.Equal(t, "info", finding.Severity)
|
|
||||||
}
|
|
||||||
assert.True(t, report.Summary.Passed)
|
assert.True(t, report.Summary.Passed)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -21,8 +21,8 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"forge.lthn.ai/core/cli/pkg/cli"
|
"forge.lthn.ai/core/cli/pkg/cli"
|
||||||
"dappco.re/go/core/lint/pkg/detect"
|
"forge.lthn.ai/core/lint/pkg/detect"
|
||||||
"dappco.re/go/core/lint/pkg/php"
|
"forge.lthn.ai/core/lint/pkg/php"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Severity styles for security output.
|
// Severity styles for security output.
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ package qa
|
||||||
import (
|
import (
|
||||||
"forge.lthn.ai/core/cli/pkg/cli"
|
"forge.lthn.ai/core/cli/pkg/cli"
|
||||||
"forge.lthn.ai/core/go-i18n"
|
"forge.lthn.ai/core/go-i18n"
|
||||||
"dappco.re/go/core/lint/locales"
|
"forge.lthn.ai/core/lint/locales"
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
|
|
|
||||||
4
go.mod
4
go.mod
|
|
@ -1,4 +1,4 @@
|
||||||
module dappco.re/go/core/lint
|
module forge.lthn.ai/core/lint
|
||||||
|
|
||||||
go 1.26.0
|
go 1.26.0
|
||||||
|
|
||||||
|
|
@ -15,7 +15,7 @@ require (
|
||||||
|
|
||||||
require (
|
require (
|
||||||
forge.lthn.ai/core/go v0.3.3 // indirect
|
forge.lthn.ai/core/go v0.3.3 // indirect
|
||||||
forge.lthn.ai/core/go-inference v0.1.6 // indirect
|
forge.lthn.ai/core/go-inference v0.1.7 // indirect
|
||||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
|
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
|
||||||
github.com/charmbracelet/bubbletea v1.3.10 // indirect
|
github.com/charmbracelet/bubbletea v1.3.10 // indirect
|
||||||
github.com/charmbracelet/colorprofile v0.4.3 // indirect
|
github.com/charmbracelet/colorprofile v0.4.3 // indirect
|
||||||
|
|
|
||||||
4
go.sum
4
go.sum
|
|
@ -4,8 +4,8 @@ forge.lthn.ai/core/go v0.3.3 h1:kYYZ2nRYy0/Be3cyuLJspRjLqTMxpckVyhb/7Sw2gd0=
|
||||||
forge.lthn.ai/core/go v0.3.3/go.mod h1:Cp4ac25pghvO2iqOu59t1GyngTKVOzKB5/VPdhRi9CQ=
|
forge.lthn.ai/core/go v0.3.3/go.mod h1:Cp4ac25pghvO2iqOu59t1GyngTKVOzKB5/VPdhRi9CQ=
|
||||||
forge.lthn.ai/core/go-i18n v0.1.7 h1:aHkAoc3W8fw3RPNvw/UszQbjyFWXHszzbZgty3SwyAA=
|
forge.lthn.ai/core/go-i18n v0.1.7 h1:aHkAoc3W8fw3RPNvw/UszQbjyFWXHszzbZgty3SwyAA=
|
||||||
forge.lthn.ai/core/go-i18n v0.1.7/go.mod h1:0VDjwtY99NSj2iqwrI09h5GUsJeM9s48MLkr+/Dn4G8=
|
forge.lthn.ai/core/go-i18n v0.1.7/go.mod h1:0VDjwtY99NSj2iqwrI09h5GUsJeM9s48MLkr+/Dn4G8=
|
||||||
forge.lthn.ai/core/go-inference v0.1.6 h1:ce42zC0zO8PuISUyAukAN1NACEdWp5wF1mRgnh5+58E=
|
forge.lthn.ai/core/go-inference v0.1.7 h1:9Dy6v03jX5ZRH3n5iTzlYyGtucuBIgSe+S7GWvBzx9Q=
|
||||||
forge.lthn.ai/core/go-inference v0.1.6/go.mod h1:jfWz+IJX55wAH98+ic6FEqqGB6/P31CHlg7VY7pxREw=
|
forge.lthn.ai/core/go-inference v0.1.7/go.mod h1:jfWz+IJX55wAH98+ic6FEqqGB6/P31CHlg7VY7pxREw=
|
||||||
forge.lthn.ai/core/go-io v0.1.7 h1:Tdb6sqh+zz1lsGJaNX9RFWM6MJ/RhSAyxfulLXrJsbk=
|
forge.lthn.ai/core/go-io v0.1.7 h1:Tdb6sqh+zz1lsGJaNX9RFWM6MJ/RhSAyxfulLXrJsbk=
|
||||||
forge.lthn.ai/core/go-io v0.1.7/go.mod h1:8lRLFk4Dnp5cR/Cyzh9WclD5566TbpdRgwcH7UZLWn4=
|
forge.lthn.ai/core/go-io v0.1.7/go.mod h1:8lRLFk4Dnp5cR/Cyzh9WclD5566TbpdRgwcH7UZLWn4=
|
||||||
forge.lthn.ai/core/go-log v0.0.4 h1:KTuCEPgFmuM8KJfnyQ8vPOU1Jg654W74h8IJvfQMfv0=
|
forge.lthn.ai/core/go-log v0.0.4 h1:KTuCEPgFmuM8KJfnyQ8vPOU1Jg654W74h8IJvfQMfv0=
|
||||||
|
|
|
||||||
5
go.work
5
go.work
|
|
@ -1,5 +0,0 @@
|
||||||
go 1.26.2
|
|
||||||
|
|
||||||
use (
|
|
||||||
.
|
|
||||||
)
|
|
||||||
2
lint.go
2
lint.go
|
|
@ -4,7 +4,7 @@ package lint
|
||||||
import (
|
import (
|
||||||
"embed"
|
"embed"
|
||||||
|
|
||||||
lintpkg "dappco.re/go/core/lint/pkg/lint"
|
lintpkg "forge.lthn.ai/core/lint/pkg/lint"
|
||||||
)
|
)
|
||||||
|
|
||||||
//go:embed catalog/*.yaml
|
//go:embed catalog/*.yaml
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,6 @@ import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
|
||||||
"io"
|
"io"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
|
@ -167,18 +166,6 @@ func (adapter CommandAdapter) Run(ctx context.Context, input RunInput, files []s
|
||||||
if !ok {
|
if !ok {
|
||||||
result.Tool.Status = "skipped"
|
result.Tool.Status = "skipped"
|
||||||
result.Tool.Duration = "0s"
|
result.Tool.Duration = "0s"
|
||||||
missingName := firstNonEmpty(adapter.Command(), adapter.name)
|
|
||||||
if missingName == "" {
|
|
||||||
missingName = adapter.name
|
|
||||||
}
|
|
||||||
result.Findings = []Finding{{
|
|
||||||
Tool: adapter.name,
|
|
||||||
Severity: "info",
|
|
||||||
Code: "missing-tool",
|
|
||||||
Message: fmt.Sprintf("%s is not installed", missingName),
|
|
||||||
Category: adapter.category,
|
|
||||||
}}
|
|
||||||
result.Tool.Findings = len(result.Findings)
|
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -189,8 +176,6 @@ func (adapter CommandAdapter) Run(ctx context.Context, input RunInput, files []s
|
||||||
|
|
||||||
args := adapter.buildArgs(input.Path, files)
|
args := adapter.buildArgs(input.Path, files)
|
||||||
stdout, stderr, exitCode, runErr := runCommand(runContext, input.Path, binary, args)
|
stdout, stderr, exitCode, runErr := runCommand(runContext, input.Path, binary, args)
|
||||||
stdout = strings.TrimSpace(stdout)
|
|
||||||
stderr = strings.TrimSpace(stderr)
|
|
||||||
|
|
||||||
result.Tool.Duration = time.Since(startedAt).Round(time.Millisecond).String()
|
result.Tool.Duration = time.Since(startedAt).Round(time.Millisecond).String()
|
||||||
|
|
||||||
|
|
@ -199,30 +184,19 @@ func (adapter CommandAdapter) Run(ctx context.Context, input RunInput, files []s
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := runContext.Err(); err != nil {
|
output := strings.TrimSpace(stdout)
|
||||||
if errors.Is(err, context.DeadlineExceeded) {
|
if strings.TrimSpace(stderr) != "" {
|
||||||
result.Tool.Status = "timeout"
|
if output != "" {
|
||||||
|
output += "\n" + strings.TrimSpace(stderr)
|
||||||
} else {
|
} else {
|
||||||
result.Tool.Status = "canceled"
|
output = strings.TrimSpace(stderr)
|
||||||
}
|
}
|
||||||
return result
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if adapter.parseOutput != nil {
|
if adapter.parseOutput != nil && output != "" {
|
||||||
if stdout != "" {
|
result.Findings = adapter.parseOutput(adapter.name, adapter.category, output)
|
||||||
result.Findings = append(result.Findings, adapter.parseOutput(adapter.name, adapter.category, stdout)...)
|
|
||||||
}
|
|
||||||
if len(result.Findings) == 0 && stderr != "" {
|
|
||||||
result.Findings = append(result.Findings, adapter.parseOutput(adapter.name, adapter.category, stderr)...)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if len(result.Findings) == 0 && (stdout != "" || stderr != "") {
|
|
||||||
output := stdout
|
|
||||||
if output != "" && stderr != "" {
|
|
||||||
output += "\n" + stderr
|
|
||||||
} else if output == "" {
|
|
||||||
output = stderr
|
|
||||||
}
|
}
|
||||||
|
if len(result.Findings) == 0 && output != "" {
|
||||||
result.Findings = parseTextDiagnostics(adapter.name, adapter.category, output)
|
result.Findings = parseTextDiagnostics(adapter.name, adapter.category, output)
|
||||||
}
|
}
|
||||||
if len(result.Findings) == 0 && runErr != nil {
|
if len(result.Findings) == 0 && runErr != nil {
|
||||||
|
|
@ -230,7 +204,7 @@ func (adapter CommandAdapter) Run(ctx context.Context, input RunInput, files []s
|
||||||
Tool: adapter.name,
|
Tool: adapter.name,
|
||||||
Severity: defaultSeverityForCategory(adapter.category),
|
Severity: defaultSeverityForCategory(adapter.category),
|
||||||
Code: "command-failed",
|
Code: "command-failed",
|
||||||
Message: strings.TrimSpace(firstNonEmpty(stdout, stderr, runErr.Error())),
|
Message: strings.TrimSpace(firstNonEmpty(output, runErr.Error())),
|
||||||
Category: adapter.category,
|
Category: adapter.category,
|
||||||
}}
|
}}
|
||||||
}
|
}
|
||||||
|
|
@ -313,7 +287,7 @@ func (CatalogAdapter) Category() string { return "correctness" }
|
||||||
|
|
||||||
func (CatalogAdapter) Fast() bool { return true }
|
func (CatalogAdapter) Fast() bool { return true }
|
||||||
|
|
||||||
func (CatalogAdapter) Run(ctx context.Context, input RunInput, files []string) AdapterResult {
|
func (CatalogAdapter) Run(_ context.Context, input RunInput, files []string) AdapterResult {
|
||||||
startedAt := time.Now()
|
startedAt := time.Now()
|
||||||
result := AdapterResult{
|
result := AdapterResult{
|
||||||
Tool: ToolRun{
|
Tool: ToolRun{
|
||||||
|
|
@ -359,9 +333,6 @@ func (CatalogAdapter) Run(ctx context.Context, input RunInput, files []string) A
|
||||||
var findings []Finding
|
var findings []Finding
|
||||||
if len(files) > 0 {
|
if len(files) > 0 {
|
||||||
for _, file := range files {
|
for _, file := range files {
|
||||||
if err := ctx.Err(); err != nil {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
scanPath := file
|
scanPath := file
|
||||||
if !filepath.IsAbs(scanPath) {
|
if !filepath.IsAbs(scanPath) {
|
||||||
scanPath = filepath.Join(input.Path, file)
|
scanPath = filepath.Join(input.Path, file)
|
||||||
|
|
@ -373,22 +344,9 @@ func (CatalogAdapter) Run(ctx context.Context, input RunInput, files []string) A
|
||||||
findings = append(findings, fileFindings...)
|
findings = append(findings, fileFindings...)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if ctx.Err() != nil {
|
|
||||||
result.Tool.Status = "canceled"
|
|
||||||
result.Tool.Duration = time.Since(startedAt).Round(time.Millisecond).String()
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
findings, _ = scanner.ScanDir(input.Path)
|
findings, _ = scanner.ScanDir(input.Path)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := ctx.Err(); err != nil {
|
|
||||||
result.Tool.Status = "canceled"
|
|
||||||
result.Tool.Duration = time.Since(startedAt).Round(time.Millisecond).String()
|
|
||||||
result.Tool.Findings = len(findings)
|
|
||||||
result.Findings = findings
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
for index := range findings {
|
for index := range findings {
|
||||||
rule := catalog.ByID(findings[index].RuleID)
|
rule := catalog.ByID(findings[index].RuleID)
|
||||||
findings[index].Tool = "catalog"
|
findings[index].Tool = "catalog"
|
||||||
|
|
@ -530,27 +488,8 @@ func parseJSONDiagnostics(tool string, category string, output string) []Finding
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if strings.TrimSpace(output) == "" {
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
if len(findings) > 0 {
|
|
||||||
findings = append(findings, Finding{
|
|
||||||
Tool: tool,
|
|
||||||
Severity: "error",
|
|
||||||
Code: "parse-error",
|
|
||||||
Message: fmt.Sprintf("failed to parse JSON output: %v", err),
|
|
||||||
Category: category,
|
|
||||||
})
|
|
||||||
return dedupeFindings(findings)
|
|
||||||
}
|
|
||||||
return []Finding{{
|
|
||||||
Tool: tool,
|
|
||||||
Severity: "error",
|
|
||||||
Code: "parse-error",
|
|
||||||
Message: fmt.Sprintf("failed to parse JSON output: %v", err),
|
|
||||||
Category: category,
|
|
||||||
}}
|
|
||||||
}
|
|
||||||
findings = append(findings, collectJSONDiagnostics(tool, category, value)...)
|
findings = append(findings, collectJSONDiagnostics(tool, category, value)...)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,372 +0,0 @@
|
||||||
package lint
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"strings"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestAdapter_CommandAdapter_Good(t *testing.T) {
|
|
||||||
binDir := t.TempDir()
|
|
||||||
writeScript(t, binDir, "demo-tool", `case "$1" in
|
|
||||||
--version|-version|version)
|
|
||||||
echo "demo-tool version 1.2.3" >&2
|
|
||||||
exit 0
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
exit 0
|
|
||||||
`)
|
|
||||||
prependPath(t, binDir)
|
|
||||||
|
|
||||||
adapter := newCommandAdapter(
|
|
||||||
"demo-tool",
|
|
||||||
[]string{"demo-tool"},
|
|
||||||
[]string{"go"},
|
|
||||||
"security",
|
|
||||||
"lint.security",
|
|
||||||
true,
|
|
||||||
true,
|
|
||||||
func(_ string, _ []string) []string {
|
|
||||||
return []string{"scan"}
|
|
||||||
},
|
|
||||||
nil,
|
|
||||||
).(CommandAdapter)
|
|
||||||
|
|
||||||
langs := adapter.Languages()
|
|
||||||
assert.Equal(t, "demo-tool", adapter.Name())
|
|
||||||
assert.True(t, adapter.Available())
|
|
||||||
assert.Equal(t, "demo-tool", adapter.Command())
|
|
||||||
assert.Equal(t, "lint.security", adapter.Entitlement())
|
|
||||||
assert.True(t, adapter.RequiresEntitlement())
|
|
||||||
assert.True(t, adapter.MatchesLanguage([]string{"go"}))
|
|
||||||
assert.True(t, adapter.MatchesLanguage([]string{"security"}))
|
|
||||||
assert.True(t, adapter.MatchesLanguage(nil))
|
|
||||||
assert.False(t, adapter.MatchesLanguage([]string{"rust"}))
|
|
||||||
assert.Equal(t, "security", adapter.Category())
|
|
||||||
assert.True(t, adapter.Fast())
|
|
||||||
assert.Equal(t, []string{"go"}, langs)
|
|
||||||
|
|
||||||
langs[0] = "mutated"
|
|
||||||
assert.Equal(t, []string{"go"}, adapter.Languages())
|
|
||||||
|
|
||||||
result := adapter.Run(context.Background(), RunInput{Path: t.TempDir()}, nil)
|
|
||||||
require.Equal(t, "passed", result.Tool.Status)
|
|
||||||
assert.Equal(t, "demo-tool version 1.2.3", result.Tool.Version)
|
|
||||||
assert.Equal(t, 0, result.Tool.Findings)
|
|
||||||
assert.Empty(t, result.Findings)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestAdapter_CommandAdapter_Bad(t *testing.T) {
|
|
||||||
adapter := CommandAdapter{
|
|
||||||
name: "missing-tool",
|
|
||||||
binaries: []string{"missing-tool"},
|
|
||||||
languages: []string{"go"},
|
|
||||||
category: "security",
|
|
||||||
}
|
|
||||||
|
|
||||||
result := adapter.Run(context.Background(), RunInput{Path: t.TempDir()}, nil)
|
|
||||||
require.Equal(t, "skipped", result.Tool.Status)
|
|
||||||
assert.Equal(t, "0s", result.Tool.Duration)
|
|
||||||
require.Len(t, result.Findings, 1)
|
|
||||||
assert.Equal(t, "info", result.Findings[0].Severity)
|
|
||||||
assert.Equal(t, "missing-tool", result.Findings[0].Code)
|
|
||||||
assert.Equal(t, "missing-tool", result.Findings[0].Tool)
|
|
||||||
assert.Equal(t, "security", result.Findings[0].Category)
|
|
||||||
assert.Equal(t, 1, result.Tool.Findings)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestAdapter_CommandAdapter_Ugly(t *testing.T) {
|
|
||||||
binDir := t.TempDir()
|
|
||||||
writeScript(t, binDir, "slow-tool", `case "$1" in
|
|
||||||
--version|-version|version)
|
|
||||||
echo "slow-tool 9.9.9"
|
|
||||||
exit 0
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
sleep 1
|
|
||||||
exit 0
|
|
||||||
`)
|
|
||||||
prependPath(t, binDir)
|
|
||||||
|
|
||||||
adapter := newCommandAdapter(
|
|
||||||
"slow-tool",
|
|
||||||
[]string{"slow-tool"},
|
|
||||||
[]string{"go"},
|
|
||||||
"security",
|
|
||||||
"",
|
|
||||||
false,
|
|
||||||
false,
|
|
||||||
func(_ string, _ []string) []string {
|
|
||||||
return []string{"scan"}
|
|
||||||
},
|
|
||||||
nil,
|
|
||||||
).(CommandAdapter)
|
|
||||||
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
result := adapter.Run(ctx, RunInput{Path: t.TempDir()}, nil)
|
|
||||||
require.Equal(t, "timeout", result.Tool.Status)
|
|
||||||
assert.Equal(t, "slow-tool 9.9.9", result.Tool.Version)
|
|
||||||
assert.Empty(t, result.Findings)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestAdapter_ParseJSONDiagnostics_Good(t *testing.T) {
|
|
||||||
output := `[
|
|
||||||
{
|
|
||||||
"location": {
|
|
||||||
"path": "internal/foo/bar.go",
|
|
||||||
"start": {"line": 42, "column": 5}
|
|
||||||
},
|
|
||||||
"message": {"text": "Errors unhandled"},
|
|
||||||
"rule_id": "G104",
|
|
||||||
"severity": "warn"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"location": {
|
|
||||||
"path": "internal/foo/bar.go",
|
|
||||||
"start": {"line": 42, "column": 5}
|
|
||||||
},
|
|
||||||
"message": {"text": "Errors unhandled"},
|
|
||||||
"rule_id": "G104",
|
|
||||||
"severity": "warn"
|
|
||||||
}
|
|
||||||
]`
|
|
||||||
|
|
||||||
findings := parseJSONDiagnostics("gosec", "security", output)
|
|
||||||
require.Len(t, findings, 1)
|
|
||||||
assert.Equal(t, "gosec", findings[0].Tool)
|
|
||||||
assert.Equal(t, "internal/foo/bar.go", findings[0].File)
|
|
||||||
assert.Equal(t, 42, findings[0].Line)
|
|
||||||
assert.Equal(t, 5, findings[0].Column)
|
|
||||||
assert.Equal(t, "G104", findings[0].Code)
|
|
||||||
assert.Equal(t, "Errors unhandled", findings[0].Message)
|
|
||||||
assert.Equal(t, "warning", findings[0].Severity)
|
|
||||||
assert.Equal(t, "security", findings[0].Category)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestAdapter_ParseJSONDiagnostics_Bad(t *testing.T) {
|
|
||||||
findings := parseJSONDiagnostics("gosec", "security", "{not json")
|
|
||||||
require.Len(t, findings, 1)
|
|
||||||
assert.Equal(t, "error", findings[0].Severity)
|
|
||||||
assert.Equal(t, "parse-error", findings[0].Code)
|
|
||||||
assert.Equal(t, "gosec", findings[0].Tool)
|
|
||||||
assert.Equal(t, "security", findings[0].Category)
|
|
||||||
assert.Contains(t, findings[0].Message, "failed to parse JSON output")
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestAdapter_ParseJSONDiagnostics_PartialOutput(t *testing.T) {
|
|
||||||
output := `[
|
|
||||||
{
|
|
||||||
"location": {
|
|
||||||
"path": "internal/foo/bar.go",
|
|
||||||
"start": {"line": 42, "column": 5}
|
|
||||||
},
|
|
||||||
"message": {"text": "Errors unhandled"},
|
|
||||||
"rule_id": "G104",
|
|
||||||
"severity": "warn"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
not json`
|
|
||||||
|
|
||||||
findings := parseJSONDiagnostics("gosec", "security", output)
|
|
||||||
require.Len(t, findings, 2)
|
|
||||||
assert.Equal(t, "G104", findings[0].Code)
|
|
||||||
assert.Equal(t, "parse-error", findings[1].Code)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestAdapter_ParseJSONDiagnostics_Ugly(t *testing.T) {
|
|
||||||
output := `[
|
|
||||||
{
|
|
||||||
"Location": {
|
|
||||||
"Path": "src/main.go",
|
|
||||||
"Start": {"Line": "7", "Column": "3"}
|
|
||||||
},
|
|
||||||
"Message": {"Text": "Potential issue"},
|
|
||||||
"RuleID": "ABC123"
|
|
||||||
}
|
|
||||||
]`
|
|
||||||
|
|
||||||
findings := parseJSONDiagnostics("scanner", "security", output)
|
|
||||||
require.Len(t, findings, 1)
|
|
||||||
assert.Equal(t, "scanner", findings[0].Tool)
|
|
||||||
assert.Equal(t, "src/main.go", findings[0].File)
|
|
||||||
assert.Equal(t, 7, findings[0].Line)
|
|
||||||
assert.Equal(t, 3, findings[0].Column)
|
|
||||||
assert.Equal(t, "ABC123", findings[0].Code)
|
|
||||||
assert.Equal(t, "Potential issue", findings[0].Message)
|
|
||||||
assert.Equal(t, "error", findings[0].Severity)
|
|
||||||
assert.Equal(t, "security", findings[0].Category)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestAdapter_CommandAdapter_JSONStdoutIgnoresStderr(t *testing.T) {
|
|
||||||
binDir := t.TempDir()
|
|
||||||
writeScript(t, binDir, "json-tool", `case "$1" in
|
|
||||||
--version|-version|version)
|
|
||||||
echo "json-tool 1.0.0"
|
|
||||||
exit 0
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
printf '%s\n' '[{"location":{"path":"src/main.go","start":{"line":12,"column":3}},"message":{"text":"boom"},"rule_id":"X1","severity":"warn"}]'
|
|
||||||
echo "debug noise" >&2
|
|
||||||
exit 0
|
|
||||||
`)
|
|
||||||
prependPath(t, binDir)
|
|
||||||
|
|
||||||
adapter := newCommandAdapter(
|
|
||||||
"json-tool",
|
|
||||||
[]string{"json-tool"},
|
|
||||||
[]string{"go"},
|
|
||||||
"security",
|
|
||||||
"",
|
|
||||||
false,
|
|
||||||
true,
|
|
||||||
func(_ string, _ []string) []string {
|
|
||||||
return []string{"scan"}
|
|
||||||
},
|
|
||||||
parseJSONDiagnostics,
|
|
||||||
).(CommandAdapter)
|
|
||||||
|
|
||||||
result := adapter.Run(context.Background(), RunInput{Path: t.TempDir()}, nil)
|
|
||||||
require.Equal(t, "failed", result.Tool.Status)
|
|
||||||
require.Len(t, result.Findings, 1)
|
|
||||||
assert.Equal(t, "X1", result.Findings[0].Code)
|
|
||||||
assert.Equal(t, "src/main.go", result.Findings[0].File)
|
|
||||||
assert.Equal(t, "boom", result.Findings[0].Message)
|
|
||||||
assert.Equal(t, "warning", result.Findings[0].Severity)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestAdapter_ParseTextDiagnostics_Good(t *testing.T) {
|
|
||||||
output := strings.Join([]string{
|
|
||||||
"src/main.go:12:3:error: boom",
|
|
||||||
"src/main.go:13:warning: caution",
|
|
||||||
}, "\n")
|
|
||||||
|
|
||||||
findings := parseTextDiagnostics("eslint", "security", output)
|
|
||||||
require.Len(t, findings, 2)
|
|
||||||
assert.Equal(t, 12, findings[0].Line)
|
|
||||||
assert.Equal(t, 3, findings[0].Column)
|
|
||||||
assert.Equal(t, "error", findings[0].Severity)
|
|
||||||
assert.Equal(t, 13, findings[1].Line)
|
|
||||||
assert.Equal(t, 0, findings[1].Column)
|
|
||||||
assert.Equal(t, "warning", findings[1].Severity)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestAdapter_ParseTextDiagnostics_Bad(t *testing.T) {
|
|
||||||
assert.Empty(t, parseTextDiagnostics("eslint", "security", ""))
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestAdapter_ParseTextDiagnostics_Ugly(t *testing.T) {
|
|
||||||
findings := parseTextDiagnostics("eslint", "security", "not parseable")
|
|
||||||
require.Len(t, findings, 1)
|
|
||||||
assert.Equal(t, "diagnostic", findings[0].Code)
|
|
||||||
assert.Equal(t, "not parseable", findings[0].Message)
|
|
||||||
assert.Equal(t, "error", findings[0].Severity)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestAdapter_ParseGovulncheckDiagnostics_Good(t *testing.T) {
|
|
||||||
output := `{"config":{"go_version":"go1.26","module_path":"example.com/app"}}
|
|
||||||
{"osv":{"id":"GO-2024-1234","summary":"Buffer overflow in foo","aliases":["CVE-2024-1234"],"affected":[{"ranges":[{"events":[{"fixed":"1.2.3"}]}]}]}}
|
|
||||||
{"finding":{"osv":"GO-2024-1234","trace":[{"package":"example.com/foo","function":"Bar"}]}}
|
|
||||||
`
|
|
||||||
|
|
||||||
findings := parseGovulncheckDiagnostics("govulncheck", "security", output)
|
|
||||||
require.Len(t, findings, 1)
|
|
||||||
assert.Equal(t, "govulncheck", findings[0].Tool)
|
|
||||||
assert.Equal(t, "example.com/foo", findings[0].File)
|
|
||||||
assert.Equal(t, "GO-2024-1234", findings[0].Code)
|
|
||||||
assert.Equal(t, "Buffer overflow in foo", findings[0].Message)
|
|
||||||
assert.Equal(t, "error", findings[0].Severity)
|
|
||||||
assert.Equal(t, "security", findings[0].Category)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestAdapter_ParseGovulncheckDiagnostics_Bad(t *testing.T) {
|
|
||||||
assert.Empty(t, parseGovulncheckDiagnostics("govulncheck", "security", "not json"))
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestAdapter_CatalogAdapter_Good(t *testing.T) {
|
|
||||||
dir := t.TempDir()
|
|
||||||
require.NoError(t, os.WriteFile(filepath.Join(dir, "input.go"), []byte(`package sample
|
|
||||||
|
|
||||||
type service struct{}
|
|
||||||
|
|
||||||
func (service) Process(string) error { return nil }
|
|
||||||
|
|
||||||
func Run() {
|
|
||||||
svc := service{}
|
|
||||||
_ = svc.Process("data")
|
|
||||||
}
|
|
||||||
`), 0o644))
|
|
||||||
|
|
||||||
adapter := CatalogAdapter{}
|
|
||||||
assert.Equal(t, "catalog", adapter.Name())
|
|
||||||
assert.True(t, adapter.Available())
|
|
||||||
assert.Equal(t, []string{"go"}, adapter.Languages())
|
|
||||||
assert.Equal(t, "catalog", adapter.Command())
|
|
||||||
assert.Empty(t, adapter.Entitlement())
|
|
||||||
assert.False(t, adapter.RequiresEntitlement())
|
|
||||||
assert.True(t, adapter.MatchesLanguage(nil))
|
|
||||||
assert.True(t, adapter.MatchesLanguage([]string{"go"}))
|
|
||||||
assert.False(t, adapter.MatchesLanguage([]string{"rust"}))
|
|
||||||
assert.Equal(t, "correctness", adapter.Category())
|
|
||||||
assert.True(t, adapter.Fast())
|
|
||||||
|
|
||||||
result := adapter.Run(context.Background(), RunInput{Path: dir}, []string{"input.go"})
|
|
||||||
require.Equal(t, "failed", result.Tool.Status)
|
|
||||||
require.Len(t, result.Findings, 1)
|
|
||||||
assert.Equal(t, "catalog", result.Findings[0].Tool)
|
|
||||||
assert.Equal(t, "go-cor-003", result.Findings[0].Code)
|
|
||||||
assert.Equal(t, "correctness", result.Findings[0].Category)
|
|
||||||
assert.Equal(t, "warning", result.Findings[0].Severity)
|
|
||||||
assert.Equal(t, "Silent error swallowing with blank identifier", result.Findings[0].Title)
|
|
||||||
assert.Equal(t, result.Findings[0].Title, result.Findings[0].Message)
|
|
||||||
|
|
||||||
filtered := adapter.Run(context.Background(), RunInput{Path: dir, Category: "security"}, []string{"input.go"})
|
|
||||||
require.Equal(t, "passed", filtered.Tool.Status)
|
|
||||||
assert.Empty(t, filtered.Findings)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestAdapter_CatalogAdapter_Bad(t *testing.T) {
|
|
||||||
adapter := CatalogAdapter{}
|
|
||||||
assert.False(t, adapter.MatchesLanguage([]string{"php"}))
|
|
||||||
assert.True(t, adapter.MatchesLanguage([]string{}))
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestAdapter_CatalogAdapter_Ugly(t *testing.T) {
|
|
||||||
dir := t.TempDir()
|
|
||||||
require.NoError(t, os.WriteFile(filepath.Join(dir, "input.go"), []byte("package sample\n"), 0o644))
|
|
||||||
|
|
||||||
adapter := CatalogAdapter{}
|
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
|
||||||
cancel()
|
|
||||||
|
|
||||||
result := adapter.Run(ctx, RunInput{Path: dir}, []string{"input.go"})
|
|
||||||
require.Equal(t, "canceled", result.Tool.Status)
|
|
||||||
assert.Empty(t, result.Findings)
|
|
||||||
}
|
|
||||||
|
|
||||||
func writeScript(t *testing.T, dir, name, body string) string {
|
|
||||||
t.Helper()
|
|
||||||
|
|
||||||
path := filepath.Join(dir, name)
|
|
||||||
content := "#!/bin/sh\n" + body
|
|
||||||
require.NoError(t, os.WriteFile(path, []byte(content), 0o755))
|
|
||||||
return path
|
|
||||||
}
|
|
||||||
|
|
||||||
func prependPath(t *testing.T, dir string) {
|
|
||||||
t.Helper()
|
|
||||||
|
|
||||||
oldPath := os.Getenv("PATH")
|
|
||||||
if oldPath == "" {
|
|
||||||
t.Setenv("PATH", dir)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
t.Setenv("PATH", dir+string(os.PathListSeparator)+oldPath)
|
|
||||||
}
|
|
||||||
|
|
@ -1,139 +0,0 @@
|
||||||
package lint
|
|
||||||
|
|
||||||
import (
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
"gopkg.in/yaml.v3"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestConfig_DefaultConfig_Good(t *testing.T) {
|
|
||||||
cfg := DefaultConfig()
|
|
||||||
|
|
||||||
assert.Equal(t, "json", cfg.Output)
|
|
||||||
assert.Equal(t, "error", cfg.FailOn)
|
|
||||||
assert.Equal(t, []string{"."}, cfg.Paths)
|
|
||||||
assert.Equal(t, []string{"vendor/", "node_modules/", ".core/"}, cfg.Exclude)
|
|
||||||
assert.Contains(t, cfg.Lint.Go, "golangci-lint")
|
|
||||||
assert.Contains(t, cfg.Lint.Security, "semgrep")
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestConfig_DefaultConfig_Bad(t *testing.T) {
|
|
||||||
cfg := DefaultConfig()
|
|
||||||
cfg.Lint.Go[0] = "mutated"
|
|
||||||
cfg.Paths[0] = "mutated"
|
|
||||||
|
|
||||||
fresh := DefaultConfig()
|
|
||||||
assert.Equal(t, "golangci-lint", fresh.Lint.Go[0])
|
|
||||||
assert.Equal(t, ".", fresh.Paths[0])
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestConfig_DefaultConfig_Ugly(t *testing.T) {
|
|
||||||
cfg := DefaultConfig()
|
|
||||||
|
|
||||||
cfg.Lint.Go = append(cfg.Lint.Go, "extra-tool")
|
|
||||||
cfg.Exclude = append(cfg.Exclude, "build/")
|
|
||||||
|
|
||||||
fresh := DefaultConfig()
|
|
||||||
assert.NotContains(t, fresh.Lint.Go, "extra-tool")
|
|
||||||
assert.NotContains(t, fresh.Exclude, "build/")
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestConfig_DefaultConfigYAML_Good(t *testing.T) {
|
|
||||||
raw, err := DefaultConfigYAML()
|
|
||||||
require.NoError(t, err)
|
|
||||||
assert.Contains(t, raw, "output: json")
|
|
||||||
assert.Contains(t, raw, "fail_on: error")
|
|
||||||
|
|
||||||
var cfg LintConfig
|
|
||||||
require.NoError(t, yaml.Unmarshal([]byte(raw), &cfg))
|
|
||||||
assert.Equal(t, DefaultConfig().Output, cfg.Output)
|
|
||||||
assert.Equal(t, DefaultConfig().FailOn, cfg.FailOn)
|
|
||||||
assert.Equal(t, DefaultConfig().Paths, cfg.Paths)
|
|
||||||
assert.Equal(t, DefaultConfig().Exclude, cfg.Exclude)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestConfig_ResolveConfigPath_Good(t *testing.T) {
|
|
||||||
assert.Equal(t, "/repo/.core/lint.yaml", ResolveConfigPath("/repo", ""))
|
|
||||||
assert.Equal(t, "/repo/config/lint.yaml", ResolveConfigPath("/repo", "config/lint.yaml"))
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestConfig_ResolveConfigPath_Bad(t *testing.T) {
|
|
||||||
assert.Equal(t, ".core/lint.yaml", ResolveConfigPath("", ""))
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestConfig_ResolveConfigPath_Ugly(t *testing.T) {
|
|
||||||
absolute := filepath.Join(t.TempDir(), "nested", "lint.yaml")
|
|
||||||
assert.Equal(t, absolute, ResolveConfigPath("/repo", absolute))
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestConfig_LoadProjectConfig_Good(t *testing.T) {
|
|
||||||
dir := t.TempDir()
|
|
||||||
require.NoError(t, os.MkdirAll(filepath.Join(dir, ".core"), 0o755))
|
|
||||||
require.NoError(t, os.WriteFile(filepath.Join(dir, ".core", "lint.yaml"), []byte(`output: sarif
|
|
||||||
fail_on: warning
|
|
||||||
paths:
|
|
||||||
- src
|
|
||||||
exclude:
|
|
||||||
- generated/
|
|
||||||
`), 0o644))
|
|
||||||
|
|
||||||
cfg, path, err := LoadProjectConfig(dir, "")
|
|
||||||
require.NoError(t, err)
|
|
||||||
assert.Equal(t, filepath.Join(dir, ".core", "lint.yaml"), path)
|
|
||||||
assert.Equal(t, "sarif", cfg.Output)
|
|
||||||
assert.Equal(t, "warning", cfg.FailOn)
|
|
||||||
assert.Equal(t, []string{"src"}, cfg.Paths)
|
|
||||||
assert.Equal(t, []string{"generated/"}, cfg.Exclude)
|
|
||||||
assert.Equal(t, "golangci-lint", cfg.Lint.Go[0])
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestConfig_LoadProjectConfig_Bad(t *testing.T) {
|
|
||||||
dir := t.TempDir()
|
|
||||||
|
|
||||||
cfg, path, err := LoadProjectConfig(dir, "")
|
|
||||||
require.NoError(t, err)
|
|
||||||
assert.Empty(t, path)
|
|
||||||
assert.Equal(t, DefaultConfig().Output, cfg.Output)
|
|
||||||
assert.Equal(t, DefaultConfig().FailOn, cfg.FailOn)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestConfig_LoadProjectConfig_Ugly(t *testing.T) {
|
|
||||||
dir := t.TempDir()
|
|
||||||
require.NoError(t, os.MkdirAll(filepath.Join(dir, ".core"), 0o755))
|
|
||||||
require.NoError(t, os.WriteFile(filepath.Join(dir, ".core", "lint.yaml"), []byte("{not: yaml"), 0o644))
|
|
||||||
|
|
||||||
_, path, err := LoadProjectConfig(dir, "")
|
|
||||||
assert.Error(t, err)
|
|
||||||
assert.Empty(t, path)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestConfig_ResolveSchedule_Good(t *testing.T) {
|
|
||||||
cfg := DefaultConfig()
|
|
||||||
cfg.Schedules = map[string]Schedule{
|
|
||||||
"nightly": {
|
|
||||||
Cron: "0 2 * * *",
|
|
||||||
Output: "json",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
schedule, err := ResolveSchedule(cfg, "nightly")
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.NotNil(t, schedule)
|
|
||||||
assert.Equal(t, "0 2 * * *", schedule.Cron)
|
|
||||||
assert.Equal(t, "json", schedule.Output)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestConfig_ResolveSchedule_Bad(t *testing.T) {
|
|
||||||
_, err := ResolveSchedule(DefaultConfig(), "missing")
|
|
||||||
assert.Error(t, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestConfig_ResolveSchedule_Ugly(t *testing.T) {
|
|
||||||
schedule, err := ResolveSchedule(DefaultConfig(), "")
|
|
||||||
require.NoError(t, err)
|
|
||||||
assert.Nil(t, schedule)
|
|
||||||
}
|
|
||||||
|
|
@ -29,34 +29,6 @@ func TestDetect_Good_ProjectMarkersAndFiles(t *testing.T) {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestDetect_Good_MarkerCoverage(t *testing.T) {
|
|
||||||
dir := t.TempDir()
|
|
||||||
|
|
||||||
files := map[string]string{
|
|
||||||
"go.mod": "module example.com/test\n",
|
|
||||||
"composer.json": "{}\n",
|
|
||||||
"package.json": "{}\n",
|
|
||||||
"tsconfig.json": "{}\n",
|
|
||||||
"requirements.txt": "ruff\n",
|
|
||||||
"pyproject.toml": "[tool.ruff]\n",
|
|
||||||
"Cargo.toml": "[package]\nname = \"test\"\n",
|
|
||||||
"Dockerfile.dev": "FROM scratch\n",
|
|
||||||
"run.sh": "#!/bin/sh\n",
|
|
||||||
"main.cpp": "int main() { return 0; }\n",
|
|
||||||
"config.yaml": "kind: Config\n",
|
|
||||||
"config.yml": "kind: Config\n",
|
|
||||||
}
|
|
||||||
|
|
||||||
for name, content := range files {
|
|
||||||
require.NoError(t, os.WriteFile(filepath.Join(dir, name), []byte(content), 0o644))
|
|
||||||
}
|
|
||||||
|
|
||||||
assert.Equal(t,
|
|
||||||
[]string{"cpp", "dockerfile", "go", "js", "php", "python", "rust", "shell", "ts", "yaml"},
|
|
||||||
Detect(dir),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestDetectFromFiles_Good(t *testing.T) {
|
func TestDetectFromFiles_Good(t *testing.T) {
|
||||||
files := []string{
|
files := []string{
|
||||||
"main.go",
|
"main.go",
|
||||||
|
|
@ -73,7 +45,7 @@ func TestDetectFromFiles_Good(t *testing.T) {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestDetect_Bad_MissingPathReturnsEmptySlice(t *testing.T) {
|
func TestDetect_MissingPathReturnsEmptySlice(t *testing.T) {
|
||||||
assert.Equal(t, []string{}, Detect(filepath.Join(t.TempDir(), "missing")))
|
assert.Equal(t, []string{}, Detect(filepath.Join(t.TempDir(), "missing")))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -85,17 +57,3 @@ func TestDetect_Good_SkipsHiddenRootDirectory(t *testing.T) {
|
||||||
|
|
||||||
assert.Equal(t, []string{}, Detect(hiddenDir))
|
assert.Equal(t, []string{}, Detect(hiddenDir))
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestDetect_Ugly_SkipsNestedHiddenAndExcludedDirectories(t *testing.T) {
|
|
||||||
dir := t.TempDir()
|
|
||||||
|
|
||||||
require.NoError(t, os.WriteFile(filepath.Join(dir, "root.go"), []byte("package main\n"), 0o644))
|
|
||||||
require.NoError(t, os.MkdirAll(filepath.Join(dir, "vendor"), 0o755))
|
|
||||||
require.NoError(t, os.WriteFile(filepath.Join(dir, "vendor", "ignored.go"), []byte("package ignored\n"), 0o644))
|
|
||||||
require.NoError(t, os.MkdirAll(filepath.Join(dir, ".core"), 0o755))
|
|
||||||
require.NoError(t, os.WriteFile(filepath.Join(dir, ".core", "ignored.go"), []byte("package ignored\n"), 0o644))
|
|
||||||
require.NoError(t, os.MkdirAll(filepath.Join(dir, "services", ".generated"), 0o755))
|
|
||||||
require.NoError(t, os.WriteFile(filepath.Join(dir, "services", ".generated", "ignored.go"), []byte("package ignored\n"), 0o644))
|
|
||||||
|
|
||||||
assert.Equal(t, []string{"go"}, Detect(dir))
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -62,26 +62,3 @@ func TestResolveRunOutputFormat_Good_ExplicitOutputBypassesConfigLoading(t *test
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.Equal(t, "sarif", format)
|
assert.Equal(t, "sarif", format)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestResolveRunOutputFormat_Bad_BrokenConfig(t *testing.T) {
|
|
||||||
dir := t.TempDir()
|
|
||||||
require.NoError(t, os.MkdirAll(filepath.Join(dir, ".core"), 0o755))
|
|
||||||
require.NoError(t, os.WriteFile(filepath.Join(dir, ".core", "lint.yaml"), []byte("{not: yaml"), 0o644))
|
|
||||||
|
|
||||||
_, err := ResolveRunOutputFormat(RunInput{
|
|
||||||
Path: dir,
|
|
||||||
})
|
|
||||||
assert.Error(t, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestResolveRunOutputFormat_Ugly_MissingSchedule(t *testing.T) {
|
|
||||||
dir := t.TempDir()
|
|
||||||
require.NoError(t, os.MkdirAll(filepath.Join(dir, ".core"), 0o755))
|
|
||||||
require.NoError(t, os.WriteFile(filepath.Join(dir, ".core", "lint.yaml"), []byte("output: text\n"), 0o644))
|
|
||||||
|
|
||||||
_, err := ResolveRunOutputFormat(RunInput{
|
|
||||||
Path: dir,
|
|
||||||
Schedule: "nightly",
|
|
||||||
})
|
|
||||||
assert.Error(t, err)
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -75,7 +75,7 @@ func WriteJSONL(w io.Writer, findings []Finding) error {
|
||||||
// WriteText writes findings in a human-readable format.
|
// WriteText writes findings in a human-readable format.
|
||||||
//
|
//
|
||||||
// lint.WriteText(os.Stdout, findings)
|
// lint.WriteText(os.Stdout, findings)
|
||||||
func WriteText(w io.Writer, findings []Finding) error {
|
func WriteText(w io.Writer, findings []Finding) {
|
||||||
for _, finding := range findings {
|
for _, finding := range findings {
|
||||||
message := finding.Message
|
message := finding.Message
|
||||||
if message == "" {
|
if message == "" {
|
||||||
|
|
@ -85,11 +85,8 @@ func WriteText(w io.Writer, findings []Finding) error {
|
||||||
if code == "" {
|
if code == "" {
|
||||||
code = finding.RuleID
|
code = finding.RuleID
|
||||||
}
|
}
|
||||||
if _, err := fmt.Fprintf(w, "%s:%d [%s] %s (%s)\n", finding.File, finding.Line, finding.Severity, message, code); err != nil {
|
fmt.Fprintf(w, "%s:%d [%s] %s (%s)\n", finding.File, finding.Line, finding.Severity, message, code)
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// WriteReportJSON writes the RFC report document as pretty-printed JSON.
|
// WriteReportJSON writes the RFC report document as pretty-printed JSON.
|
||||||
|
|
@ -104,18 +101,15 @@ func WriteReportJSON(w io.Writer, report Report) error {
|
||||||
// WriteReportText writes report findings followed by a short summary.
|
// WriteReportText writes report findings followed by a short summary.
|
||||||
//
|
//
|
||||||
// lint.WriteReportText(os.Stdout, report)
|
// lint.WriteReportText(os.Stdout, report)
|
||||||
func WriteReportText(w io.Writer, report Report) error {
|
func WriteReportText(w io.Writer, report Report) {
|
||||||
if err := WriteText(w, report.Findings); err != nil {
|
WriteText(w, report.Findings)
|
||||||
return err
|
fmt.Fprintf(w, "\n%d finding(s): %d error(s), %d warning(s), %d info\n", report.Summary.Total, report.Summary.Errors, report.Summary.Warnings, report.Summary.Info)
|
||||||
}
|
|
||||||
_, err := fmt.Fprintf(w, "\n%d finding(s): %d error(s), %d warning(s), %d info\n", report.Summary.Total, report.Summary.Errors, report.Summary.Warnings, report.Summary.Info)
|
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// WriteReportGitHub writes GitHub Actions annotation lines.
|
// WriteReportGitHub writes GitHub Actions annotation lines.
|
||||||
//
|
//
|
||||||
// lint.WriteReportGitHub(os.Stdout, report)
|
// lint.WriteReportGitHub(os.Stdout, report)
|
||||||
func WriteReportGitHub(w io.Writer, report Report) error {
|
func WriteReportGitHub(w io.Writer, report Report) {
|
||||||
for _, finding := range report.Findings {
|
for _, finding := range report.Findings {
|
||||||
level := githubAnnotationLevel(finding.Severity)
|
level := githubAnnotationLevel(finding.Severity)
|
||||||
|
|
||||||
|
|
@ -138,11 +132,8 @@ func WriteReportGitHub(w io.Writer, report Report) error {
|
||||||
if code == "" {
|
if code == "" {
|
||||||
code = finding.RuleID
|
code = finding.RuleID
|
||||||
}
|
}
|
||||||
if _, err := fmt.Fprintf(w, "::%s%s::[%s] %s (%s)\n", level, location, finding.Tool, message, code); err != nil {
|
fmt.Fprintf(w, "::%s%s::[%s] %s (%s)\n", level, location, finding.Tool, message, code)
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// WriteReportSARIF writes a minimal SARIF document for code scanning tools.
|
// WriteReportSARIF writes a minimal SARIF document for code scanning tools.
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,6 @@ package lint
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
|
@ -58,19 +57,6 @@ func TestSummarise_Good_Empty(t *testing.T) {
|
||||||
assert.Empty(t, summary.BySeverity)
|
assert.Empty(t, summary.BySeverity)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestSummarise_Bad_BlankSeverityDefaultsToWarning(t *testing.T) {
|
|
||||||
summary := Summarise([]Finding{
|
|
||||||
{Severity: ""},
|
|
||||||
{Severity: "info"},
|
|
||||||
})
|
|
||||||
|
|
||||||
assert.Equal(t, 2, summary.Total)
|
|
||||||
assert.Equal(t, 1, summary.Warnings)
|
|
||||||
assert.Equal(t, 1, summary.Info)
|
|
||||||
assert.Equal(t, 0, summary.Errors)
|
|
||||||
assert.True(t, summary.Passed)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestWriteJSON_Good_Roundtrip(t *testing.T) {
|
func TestWriteJSON_Good_Roundtrip(t *testing.T) {
|
||||||
findings := sampleFindings()
|
findings := sampleFindings()
|
||||||
var buf bytes.Buffer
|
var buf bytes.Buffer
|
||||||
|
|
@ -129,16 +115,10 @@ func TestWriteJSONL_Good_Empty(t *testing.T) {
|
||||||
assert.Empty(t, buf.String())
|
assert.Empty(t, buf.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestWriteJSONL_Bad_PropagatesWriterErrors(t *testing.T) {
|
|
||||||
err := WriteJSONL(failingWriter{}, sampleFindings())
|
|
||||||
require.Error(t, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestWriteText_Good(t *testing.T) {
|
func TestWriteText_Good(t *testing.T) {
|
||||||
findings := sampleFindings()
|
findings := sampleFindings()
|
||||||
var buf bytes.Buffer
|
var buf bytes.Buffer
|
||||||
err := WriteText(&buf, findings)
|
WriteText(&buf, findings)
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
output := buf.String()
|
output := buf.String()
|
||||||
assert.Contains(t, output, "store/query.go:42")
|
assert.Contains(t, output, "store/query.go:42")
|
||||||
|
|
@ -151,15 +131,14 @@ func TestWriteText_Good(t *testing.T) {
|
||||||
|
|
||||||
func TestWriteText_Good_Empty(t *testing.T) {
|
func TestWriteText_Good_Empty(t *testing.T) {
|
||||||
var buf bytes.Buffer
|
var buf bytes.Buffer
|
||||||
err := WriteText(&buf, nil)
|
WriteText(&buf, nil)
|
||||||
require.NoError(t, err)
|
|
||||||
assert.Empty(t, buf.String())
|
assert.Empty(t, buf.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestWriteReportGitHub_Good_MapsInfoToNotice(t *testing.T) {
|
func TestWriteReportGitHub_Good_MapsInfoToNotice(t *testing.T) {
|
||||||
var buf bytes.Buffer
|
var buf bytes.Buffer
|
||||||
|
|
||||||
err := WriteReportGitHub(&buf, Report{
|
WriteReportGitHub(&buf, Report{
|
||||||
Findings: []Finding{{
|
Findings: []Finding{{
|
||||||
Tool: "demo",
|
Tool: "demo",
|
||||||
File: "example.go",
|
File: "example.go",
|
||||||
|
|
@ -170,23 +149,10 @@ func TestWriteReportGitHub_Good_MapsInfoToNotice(t *testing.T) {
|
||||||
Message: "explanation",
|
Message: "explanation",
|
||||||
}},
|
}},
|
||||||
})
|
})
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
assert.Contains(t, buf.String(), "::notice file=example.go,line=7,col=3::[demo] explanation (demo-rule)")
|
assert.Contains(t, buf.String(), "::notice file=example.go,line=7,col=3::[demo] explanation (demo-rule)")
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestWriteText_Bad_PropagatesWriterErrors(t *testing.T) {
|
|
||||||
err := WriteText(failingWriter{}, sampleFindings())
|
|
||||||
require.Error(t, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestWriteReportGitHub_Bad_PropagatesWriterErrors(t *testing.T) {
|
|
||||||
err := WriteReportGitHub(failingWriter{}, Report{
|
|
||||||
Findings: sampleFindings(),
|
|
||||||
})
|
|
||||||
require.Error(t, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestWriteReportSARIF_Good_MapsInfoToNote(t *testing.T) {
|
func TestWriteReportSARIF_Good_MapsInfoToNote(t *testing.T) {
|
||||||
var buf bytes.Buffer
|
var buf bytes.Buffer
|
||||||
|
|
||||||
|
|
@ -210,61 +176,3 @@ func TestWriteReportSARIF_Good_MapsInfoToNote(t *testing.T) {
|
||||||
results := runs[0].(map[string]any)["results"].([]any)
|
results := runs[0].(map[string]any)["results"].([]any)
|
||||||
assert.Equal(t, "note", results[0].(map[string]any)["level"])
|
assert.Equal(t, "note", results[0].(map[string]any)["level"])
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestWriteReportJSON_Good_Roundtrip(t *testing.T) {
|
|
||||||
var buf bytes.Buffer
|
|
||||||
|
|
||||||
err := WriteReportJSON(&buf, Report{
|
|
||||||
Project: "demo",
|
|
||||||
Languages: []string{"go"},
|
|
||||||
Findings: []Finding{{
|
|
||||||
Tool: "demo",
|
|
||||||
File: "example.go",
|
|
||||||
Line: 7,
|
|
||||||
Column: 3,
|
|
||||||
Severity: "warning",
|
|
||||||
Code: "demo-rule",
|
|
||||||
Message: "explanation",
|
|
||||||
}},
|
|
||||||
Summary: Summary{Total: 1, Warnings: 1, Passed: true},
|
|
||||||
})
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
var decoded Report
|
|
||||||
require.NoError(t, json.Unmarshal(buf.Bytes(), &decoded))
|
|
||||||
assert.Equal(t, "demo", decoded.Project)
|
|
||||||
assert.Equal(t, []string{"go"}, decoded.Languages)
|
|
||||||
require.Len(t, decoded.Findings, 1)
|
|
||||||
assert.Equal(t, "demo-rule", decoded.Findings[0].Code)
|
|
||||||
assert.Equal(t, 1, decoded.Summary.Total)
|
|
||||||
assert.Equal(t, 1, decoded.Summary.Warnings)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestWriteReportText_Good_IncludesSummary(t *testing.T) {
|
|
||||||
var buf bytes.Buffer
|
|
||||||
|
|
||||||
err := WriteReportText(&buf, Report{
|
|
||||||
Findings: sampleFindings(),
|
|
||||||
Summary: Summarise(sampleFindings()),
|
|
||||||
})
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
output := buf.String()
|
|
||||||
assert.Contains(t, output, "store/query.go:42")
|
|
||||||
assert.Contains(t, output, "3 finding(s):")
|
|
||||||
assert.Contains(t, output, "0 error(s), 3 warning(s), 0 info")
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestWriteReportText_Bad_PropagatesWriterErrors(t *testing.T) {
|
|
||||||
err := WriteReportText(failingWriter{}, Report{
|
|
||||||
Findings: sampleFindings(),
|
|
||||||
Summary: Summarise(sampleFindings()),
|
|
||||||
})
|
|
||||||
require.Error(t, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
type failingWriter struct{}
|
|
||||||
|
|
||||||
func (failingWriter) Write([]byte) (int, error) {
|
|
||||||
return 0, errors.New("write failed")
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -29,9 +29,6 @@ func TestDetectLanguage_Good(t *testing.T) {
|
||||||
{"noextension", ""},
|
{"noextension", ""},
|
||||||
{"file.py", "python"},
|
{"file.py", "python"},
|
||||||
{"Dockerfile", "dockerfile"},
|
{"Dockerfile", "dockerfile"},
|
||||||
{"services/Dockerfile.prod", "dockerfile"},
|
|
||||||
{"configs/settings.yaml", "yaml"},
|
|
||||||
{"configs/settings.yml", "yaml"},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
|
|
@ -42,15 +39,6 @@ func TestDetectLanguage_Good(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestDetectLanguage_Bad_UnknownExtension(t *testing.T) {
|
|
||||||
assert.Equal(t, "", DetectLanguage("notes.txt"))
|
|
||||||
assert.Equal(t, "", DetectLanguage("README"))
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestDetectLanguage_Ugly_DockerfileVariant(t *testing.T) {
|
|
||||||
assert.Equal(t, "dockerfile", DetectLanguage("nested/Dockerfile.test"))
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestScanDir_Good_FindsMatches(t *testing.T) {
|
func TestScanDir_Good_FindsMatches(t *testing.T) {
|
||||||
dir := t.TempDir()
|
dir := t.TempDir()
|
||||||
|
|
||||||
|
|
@ -221,58 +209,6 @@ func TestScanFile_Good_Python(t *testing.T) {
|
||||||
assert.Equal(t, "python", DetectLanguage(file))
|
assert.Equal(t, "python", DetectLanguage(file))
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestScanFile_Bad_NoMatchingLanguageRules(t *testing.T) {
|
|
||||||
dir := t.TempDir()
|
|
||||||
file := filepath.Join(dir, "app.go")
|
|
||||||
err := os.WriteFile(file, []byte("package main\n"), 0o644)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
rules := []Rule{
|
|
||||||
{
|
|
||||||
ID: "php-only",
|
|
||||||
Title: "PHP TODO",
|
|
||||||
Severity: "low",
|
|
||||||
Languages: []string{"php"},
|
|
||||||
Pattern: `TODO`,
|
|
||||||
Fix: "Remove TODO",
|
|
||||||
Detection: "regex",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
s, err := NewScanner(rules)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
findings, err := s.ScanFile(file)
|
|
||||||
require.NoError(t, err)
|
|
||||||
assert.Empty(t, findings)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestScanFile_Ugly_UnsupportedExtension(t *testing.T) {
|
|
||||||
dir := t.TempDir()
|
|
||||||
file := filepath.Join(dir, "notes.txt")
|
|
||||||
err := os.WriteFile(file, []byte("TODO: this is not a recognised source file\n"), 0o644)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
rules := []Rule{
|
|
||||||
{
|
|
||||||
ID: "go-only",
|
|
||||||
Title: "Go TODO",
|
|
||||||
Severity: "low",
|
|
||||||
Languages: []string{"go"},
|
|
||||||
Pattern: `TODO`,
|
|
||||||
Fix: "Remove TODO",
|
|
||||||
Detection: "regex",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
s, err := NewScanner(rules)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
findings, err := s.ScanFile(file)
|
|
||||||
require.NoError(t, err)
|
|
||||||
assert.Nil(t, findings)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestScanDir_Good_Subdirectories(t *testing.T) {
|
func TestScanDir_Good_Subdirectories(t *testing.T) {
|
||||||
dir := t.TempDir()
|
dir := t.TempDir()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -144,9 +144,6 @@ func (service *Service) Run(ctx context.Context, input RunInput) (Report, error)
|
||||||
var toolRuns []ToolRun
|
var toolRuns []ToolRun
|
||||||
|
|
||||||
for _, adapter := range selectedAdapters {
|
for _, adapter := range selectedAdapters {
|
||||||
if err := ctx.Err(); err != nil {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
if input.Hook && !adapter.Fast() {
|
if input.Hook && !adapter.Fast() {
|
||||||
toolRuns = append(toolRuns, ToolRun{
|
toolRuns = append(toolRuns, ToolRun{
|
||||||
Name: adapter.Name(),
|
Name: adapter.Name(),
|
||||||
|
|
|
||||||
|
|
@ -501,32 +501,6 @@ exit 0
|
||||||
assert.Equal(t, "prettier 3.2.1", report.Tools[0].Version)
|
assert.Equal(t, "prettier 3.2.1", report.Tools[0].Version)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestServiceRun_Good_ReportsMissingToolAsInfoFinding(t *testing.T) {
|
|
||||||
dir := t.TempDir()
|
|
||||||
require.NoError(t, os.WriteFile(filepath.Join(dir, "composer.json"), []byte("{\n \"name\": \"example/test\"\n}\n"), 0o644))
|
|
||||||
require.NoError(t, os.WriteFile(filepath.Join(dir, "index.php"), []byte("<?php\n"), 0o644))
|
|
||||||
require.NoError(t, os.MkdirAll(filepath.Join(dir, ".core"), 0o755))
|
|
||||||
require.NoError(t, os.WriteFile(filepath.Join(dir, ".core", "lint.yaml"), []byte("lint:\n php:\n - missing-tool\n"), 0o644))
|
|
||||||
|
|
||||||
svc := &Service{adapters: []Adapter{
|
|
||||||
newCommandAdapter("missing-tool", []string{"definitely-not-installed-xyz"}, []string{"php"}, "correctness", "", false, true, projectPathArguments(), parseTextDiagnostics),
|
|
||||||
}}
|
|
||||||
report, err := svc.Run(context.Background(), RunInput{
|
|
||||||
Path: dir,
|
|
||||||
FailOn: "warning",
|
|
||||||
})
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
require.Len(t, report.Tools, 1)
|
|
||||||
require.Len(t, report.Findings, 1)
|
|
||||||
assert.Equal(t, "skipped", report.Tools[0].Status)
|
|
||||||
assert.Equal(t, "info", report.Findings[0].Severity)
|
|
||||||
assert.Equal(t, "missing-tool", report.Findings[0].Code)
|
|
||||||
assert.Equal(t, "definitely-not-installed-xyz is not installed", report.Findings[0].Message)
|
|
||||||
assert.Equal(t, 1, report.Summary.Info)
|
|
||||||
assert.True(t, report.Summary.Passed)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestServiceRun_Good_DeduplicatesMergedFindings(t *testing.T) {
|
func TestServiceRun_Good_DeduplicatesMergedFindings(t *testing.T) {
|
||||||
dir := t.TempDir()
|
dir := t.TempDir()
|
||||||
require.NoError(t, os.WriteFile(filepath.Join(dir, "go.mod"), []byte("module example.com/test\n"), 0o644))
|
require.NoError(t, os.WriteFile(filepath.Join(dir, "go.mod"), []byte("module example.com/test\n"), 0o644))
|
||||||
|
|
@ -566,38 +540,6 @@ func TestServiceTools_EmptyInventoryReturnsEmptySlice(t *testing.T) {
|
||||||
assert.Empty(t, tools)
|
assert.Empty(t, tools)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestServiceRun_Good_StopsDispatchingAfterContextCancel(t *testing.T) {
|
|
||||||
dir := t.TempDir()
|
|
||||||
require.NoError(t, os.WriteFile(filepath.Join(dir, "composer.json"), []byte("{\n \"name\": \"example/test\"\n}\n"), 0o644))
|
|
||||||
require.NoError(t, os.MkdirAll(filepath.Join(dir, ".core"), 0o755))
|
|
||||||
require.NoError(t, os.WriteFile(filepath.Join(dir, ".core", "lint.yaml"), []byte(`lint:
|
|
||||||
php:
|
|
||||||
- first
|
|
||||||
- second
|
|
||||||
`), 0o644))
|
|
||||||
|
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
var secondRan bool
|
|
||||||
svc := &Service{adapters: []Adapter{
|
|
||||||
cancellingAdapter{name: "first", cancel: cancel},
|
|
||||||
recordingAdapter{name: "second", ran: &secondRan},
|
|
||||||
}}
|
|
||||||
|
|
||||||
report, err := svc.Run(ctx, RunInput{
|
|
||||||
Path: dir,
|
|
||||||
Lang: "php",
|
|
||||||
FailOn: "warning",
|
|
||||||
})
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
require.Len(t, report.Tools, 1)
|
|
||||||
assert.Equal(t, "first", report.Tools[0].Name)
|
|
||||||
assert.False(t, secondRan)
|
|
||||||
assert.Empty(t, report.Findings)
|
|
||||||
}
|
|
||||||
|
|
||||||
type shortcutAdapter struct {
|
type shortcutAdapter struct {
|
||||||
name string
|
name string
|
||||||
category string
|
category string
|
||||||
|
|
@ -631,92 +573,6 @@ func (adapter shortcutAdapter) Run(_ context.Context, _ RunInput, _ []string) Ad
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type recordingAdapter struct {
|
|
||||||
name string
|
|
||||||
ran *bool
|
|
||||||
}
|
|
||||||
|
|
||||||
func (adapter recordingAdapter) Name() string { return adapter.name }
|
|
||||||
|
|
||||||
func (adapter recordingAdapter) Available() bool { return true }
|
|
||||||
|
|
||||||
func (adapter recordingAdapter) Languages() []string { return []string{"php"} }
|
|
||||||
|
|
||||||
func (adapter recordingAdapter) Command() string { return adapter.name }
|
|
||||||
|
|
||||||
func (adapter recordingAdapter) Entitlement() string { return "" }
|
|
||||||
|
|
||||||
func (adapter recordingAdapter) RequiresEntitlement() bool { return false }
|
|
||||||
|
|
||||||
func (adapter recordingAdapter) MatchesLanguage(languages []string) bool {
|
|
||||||
for _, language := range languages {
|
|
||||||
if language == "php" {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func (adapter recordingAdapter) Category() string { return "correctness" }
|
|
||||||
|
|
||||||
func (adapter recordingAdapter) Fast() bool { return true }
|
|
||||||
|
|
||||||
func (adapter recordingAdapter) Run(_ context.Context, _ RunInput, _ []string) AdapterResult {
|
|
||||||
if adapter.ran != nil {
|
|
||||||
*adapter.ran = true
|
|
||||||
}
|
|
||||||
return AdapterResult{
|
|
||||||
Tool: ToolRun{
|
|
||||||
Name: adapter.name,
|
|
||||||
Status: "passed",
|
|
||||||
Duration: "0s",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type cancellingAdapter struct {
|
|
||||||
name string
|
|
||||||
cancel context.CancelFunc
|
|
||||||
}
|
|
||||||
|
|
||||||
func (adapter cancellingAdapter) Name() string { return adapter.name }
|
|
||||||
|
|
||||||
func (adapter cancellingAdapter) Available() bool { return true }
|
|
||||||
|
|
||||||
func (adapter cancellingAdapter) Languages() []string { return []string{"php"} }
|
|
||||||
|
|
||||||
func (adapter cancellingAdapter) Command() string { return adapter.name }
|
|
||||||
|
|
||||||
func (adapter cancellingAdapter) Entitlement() string { return "" }
|
|
||||||
|
|
||||||
func (adapter cancellingAdapter) RequiresEntitlement() bool { return false }
|
|
||||||
|
|
||||||
func (adapter cancellingAdapter) MatchesLanguage(languages []string) bool {
|
|
||||||
for _, language := range languages {
|
|
||||||
if language == "php" {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func (adapter cancellingAdapter) Category() string { return "correctness" }
|
|
||||||
|
|
||||||
func (adapter cancellingAdapter) Fast() bool { return true }
|
|
||||||
|
|
||||||
func (adapter cancellingAdapter) Run(_ context.Context, _ RunInput, _ []string) AdapterResult {
|
|
||||||
if adapter.cancel != nil {
|
|
||||||
adapter.cancel()
|
|
||||||
}
|
|
||||||
return AdapterResult{
|
|
||||||
Tool: ToolRun{
|
|
||||||
Name: adapter.name,
|
|
||||||
Status: "passed",
|
|
||||||
Duration: "0s",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type duplicateAdapter struct {
|
type duplicateAdapter struct {
|
||||||
name string
|
name string
|
||||||
finding Finding
|
finding Finding
|
||||||
|
|
|
||||||
|
|
@ -111,7 +111,7 @@ func ParseVulnCheckJSON(stdout, stderr string) (*VulnResult, error) {
|
||||||
|
|
||||||
var msg govulncheckMessage
|
var msg govulncheckMessage
|
||||||
if err := json.Unmarshal([]byte(line), &msg); err != nil {
|
if err := json.Unmarshal([]byte(line), &msg); err != nil {
|
||||||
return nil, coreerr.E("ParseVulnCheckJSON", "invalid govulncheck JSON output", err)
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if msg.Config != nil {
|
if msg.Config != nil {
|
||||||
|
|
|
||||||
|
|
@ -43,8 +43,11 @@ also not json
|
||||||
{"finding":{"osv":"GO-2024-5678","trace":[{"package":"example.com/dep","function":"Fn"}]}}
|
{"finding":{"osv":"GO-2024-5678","trace":[{"package":"example.com/dep","function":"Fn"}]}}
|
||||||
`
|
`
|
||||||
result, err := ParseVulnCheckJSON(stdout, "")
|
result, err := ParseVulnCheckJSON(stdout, "")
|
||||||
require.Error(t, err)
|
require.NoError(t, err)
|
||||||
assert.Nil(t, result)
|
assert.Equal(t, "example.com/app", result.Module)
|
||||||
|
require.Len(t, result.Findings, 1)
|
||||||
|
assert.Equal(t, "GO-2024-5678", result.Findings[0].ID)
|
||||||
|
assert.Equal(t, "Test vuln", result.Findings[0].Description)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestParseVulnCheckJSON_Empty(t *testing.T) {
|
func TestParseVulnCheckJSON_Empty(t *testing.T) {
|
||||||
|
|
|
||||||
BIN
tests/.DS_Store
vendored
BIN
tests/.DS_Store
vendored
Binary file not shown.
BIN
tests/cli/.DS_Store
vendored
BIN
tests/cli/.DS_Store
vendored
Binary file not shown.
BIN
tests/cli/qa/.DS_Store
vendored
BIN
tests/cli/qa/.DS_Store
vendored
Binary file not shown.
|
|
@ -2,7 +2,7 @@ package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"forge.lthn.ai/core/cli/pkg/cli"
|
"forge.lthn.ai/core/cli/pkg/cli"
|
||||||
_ "dappco.re/go/core/lint/cmd/qa"
|
_ "forge.lthn.ai/core/lint/cmd/qa"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue