diff --git a/archive/internal/archtest/scan_test.go b/archive/internal/archtest/scan_test.go new file mode 100644 index 00000000..2bdacf3c --- /dev/null +++ b/archive/internal/archtest/scan_test.go @@ -0,0 +1,91 @@ +package archtest + +import ( + "os" + "path/filepath" + "strings" + "testing" +) + +// TestScanDetectsInjectedViolations ist der geforderte Nachweis (Prüfung 1): +// ein absichtlich eingefügter Verstoß muss den Scanner nachweislich zum +// Fund bringen - für beide Regeln, plus den Beleg, dass eine begründete +// archtest:allow-Ausnahme direkt über der Fundstelle den Fund unterdrückt. +func TestScanDetectsInjectedViolations(t *testing.T) { + dir := t.TempDir() + src := `package fixture + +import ( + "encoding/json" + "fmt" + "net/http" +) + +func leak(w http.ResponseWriter, err error) { + w.Write([]byte(err.Error())) +} + +func leakFprintf(w http.ResponseWriter, err error) { + fmt.Fprintf(w, "%v", err.Error()) +} + +func leakEncode(w http.ResponseWriter, err error) { + _ = json.NewEncoder(w).Encode(map[string]string{"error": err.Error()}) +} + +func allowedLeak(w http.ResponseWriter, err error) { + // archtest:allow Testfixtur - Nachweis, dass eine direkt darueberstehende, begruendete Ausnahme greift + w.Write([]byte(err.Error())) +} + +func rawServer() { + http.ListenAndServe(":8080", nil) +} +` + if err := os.WriteFile(filepath.Join(dir, "fixture.go"), []byte(src), 0o644); err != nil { + t.Fatalf("Testfixtur schreiben: %v", err) + } + allowedCallLine := lineNumberOf(src, "w.Write([]byte(err.Error()))", 2) // die zweite (zulässige) Vorkommnis + + findings, err := Scan(dir, nil) + if err != nil { + t.Fatalf("Scan: %v", err) + } + + var leakLines, rawLines []int + for _, f := range findings { + switch f.Rule { + case "err-in-response": + leakLines = append(leakLines, f.Line) + case "raw-listen-and-serve": + rawLines = append(rawLines, f.Line) + } + } + + if len(leakLines) != 3 { + t.Errorf("erwarte 3 err-in-response-Funde (Write, Fprintf, Encode), habe %d: %v", len(leakLines), leakLines) + } + if len(rawLines) != 1 { + t.Errorf("erwarte 1 raw-listen-and-serve-Fund, habe %d: %v", len(rawLines), rawLines) + } + for _, l := range leakLines { + if l == allowedCallLine { + t.Errorf("Zeile %d mit direkt darueberstehender, begruendeter archtest:allow-Ausnahme haette nicht gefunden werden duerfen", l) + } + } +} + +// lineNumberOf gibt die 1-basierte Zeilennummer des n-ten Vorkommens von +// needle in src zurueck (n=1 fuer das erste), oder 0 wenn nicht gefunden. +func lineNumberOf(src, needle string, n int) int { + count := 0 + for i, line := range strings.Split(src, "\n") { + if strings.Contains(line, needle) { + count++ + if count == n { + return i + 1 + } + } + } + return 0 +} diff --git a/internal/archtest/scan_test.go b/internal/archtest/scan_test.go new file mode 100644 index 00000000..2bdacf3c --- /dev/null +++ b/internal/archtest/scan_test.go @@ -0,0 +1,91 @@ +package archtest + +import ( + "os" + "path/filepath" + "strings" + "testing" +) + +// TestScanDetectsInjectedViolations ist der geforderte Nachweis (Prüfung 1): +// ein absichtlich eingefügter Verstoß muss den Scanner nachweislich zum +// Fund bringen - für beide Regeln, plus den Beleg, dass eine begründete +// archtest:allow-Ausnahme direkt über der Fundstelle den Fund unterdrückt. +func TestScanDetectsInjectedViolations(t *testing.T) { + dir := t.TempDir() + src := `package fixture + +import ( + "encoding/json" + "fmt" + "net/http" +) + +func leak(w http.ResponseWriter, err error) { + w.Write([]byte(err.Error())) +} + +func leakFprintf(w http.ResponseWriter, err error) { + fmt.Fprintf(w, "%v", err.Error()) +} + +func leakEncode(w http.ResponseWriter, err error) { + _ = json.NewEncoder(w).Encode(map[string]string{"error": err.Error()}) +} + +func allowedLeak(w http.ResponseWriter, err error) { + // archtest:allow Testfixtur - Nachweis, dass eine direkt darueberstehende, begruendete Ausnahme greift + w.Write([]byte(err.Error())) +} + +func rawServer() { + http.ListenAndServe(":8080", nil) +} +` + if err := os.WriteFile(filepath.Join(dir, "fixture.go"), []byte(src), 0o644); err != nil { + t.Fatalf("Testfixtur schreiben: %v", err) + } + allowedCallLine := lineNumberOf(src, "w.Write([]byte(err.Error()))", 2) // die zweite (zulässige) Vorkommnis + + findings, err := Scan(dir, nil) + if err != nil { + t.Fatalf("Scan: %v", err) + } + + var leakLines, rawLines []int + for _, f := range findings { + switch f.Rule { + case "err-in-response": + leakLines = append(leakLines, f.Line) + case "raw-listen-and-serve": + rawLines = append(rawLines, f.Line) + } + } + + if len(leakLines) != 3 { + t.Errorf("erwarte 3 err-in-response-Funde (Write, Fprintf, Encode), habe %d: %v", len(leakLines), leakLines) + } + if len(rawLines) != 1 { + t.Errorf("erwarte 1 raw-listen-and-serve-Fund, habe %d: %v", len(rawLines), rawLines) + } + for _, l := range leakLines { + if l == allowedCallLine { + t.Errorf("Zeile %d mit direkt darueberstehender, begruendeter archtest:allow-Ausnahme haette nicht gefunden werden duerfen", l) + } + } +} + +// lineNumberOf gibt die 1-basierte Zeilennummer des n-ten Vorkommens von +// needle in src zurueck (n=1 fuer das erste), oder 0 wenn nicht gefunden. +func lineNumberOf(src, needle string, n int) int { + count := 0 + for i, line := range strings.Split(src, "\n") { + if strings.Contains(line, needle) { + count++ + if count == n { + return i + 1 + } + } + } + return 0 +}