feat(14-01): add 5 CI/CD log sources (GitHubActions, TravisCI, CircleCI, Jenkins, GitLabCI)

- GitHubActionsSource: searches GitHub code search for workflow files with provider keywords (token-gated)
- TravisCISource: queries Travis CI v3 API for public build logs (credentialless)
- CircleCISource: queries CircleCI v2 pipeline API for build pipelines (token-gated)
- JenkinsSource: queries open Jenkins /api/json for job build consoles (credentialless)
- GitLabCISource: queries GitLab projects API for CI-enabled projects (token-gated)
- RegisterAll extended to 45 sources (40 Phase 10-13 + 5 Phase 14)
- Integration test updated with fixtures for all 5 new sources
- cmd/recon.go wires CIRCLECI_TOKEN env var
This commit is contained in:
salvacybersec
2026-04-06 13:17:31 +03:00
parent dc90785ab0
commit e0f267f7bf
14 changed files with 1303 additions and 12 deletions

View File

@@ -312,6 +312,36 @@ func TestIntegration_AllSources_SweepAll(t *testing.T) {
_, _ = w.Write([]byte(`{"packages":[{"package_id":"chart-1","name":"leaked-chart","normalized_name":"leaked-chart","repository":{"name":"bitnami","kind":0}}]}`))
})
// ---- Phase 14: GitHub Actions /ghactions/search/code ----
mux.HandleFunc("/ghactions/search/code", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(`{"items":[{"html_url":"https://github.com/alice/repo/.github/workflows/ci.yml","repository":{"full_name":"alice/repo"}}]}`))
})
// ---- Phase 14: Travis CI /travis/builds ----
mux.HandleFunc("/travis/builds", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(`{"builds":[{"id":12345,"state":"passed","repository":{"slug":"alice/project"}}]}`))
})
// ---- Phase 14: CircleCI /circle/pipeline ----
mux.HandleFunc("/circle/pipeline", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(`{"items":[{"id":"pipeline-uuid-1","vcs":{"provider_name":"github","target_repository_url":"https://github.com/alice/repo"}}]}`))
})
// ---- Phase 14: Jenkins /jenkins/api/json ----
mux.HandleFunc("/jenkins/api/json", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(`{"jobs":[{"name":"build-api","url":"https://jenkins.example.com/job/build-api/","lastBuild":{"number":42,"url":"https://jenkins.example.com/job/build-api/42/"}}]}`))
})
// ---- Phase 14: GitLab CI /gitlabci/api/v4/projects ----
mux.HandleFunc("/gitlabci/api/v4/projects", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(`[{"id":100,"path_with_namespace":"alice/project","web_url":"https://gitlab.com/alice/project"}]`))
})
srv := httptest.NewServer(mux)
defer srv.Close()
@@ -550,9 +580,50 @@ func TestIntegration_AllSources_SweepAll(t *testing.T) {
// helm
eng.Register(&HelmSource{BaseURL: srv.URL + "/helm", Registry: reg, Limiters: lim, Client: NewClient()})
// Sanity: all 40 sources registered.
if n := len(eng.List()); n != 40 {
t.Fatalf("expected 40 sources on engine, got %d: %v", n, eng.List())
// --- Phase 14: CI/CD log sources ---
// GitHub Actions
eng.Register(&GitHubActionsSource{
Token: "ghp-test",
BaseURL: srv.URL + "/ghactions",
Registry: reg,
Limiters: lim,
client: NewClient(),
})
// Travis CI
eng.Register(&TravisCISource{
BaseURL: srv.URL + "/travis",
Registry: reg,
Limiters: lim,
Client: NewClient(),
})
// CircleCI
eng.Register(&CircleCISource{
Token: "test-circle-token",
BaseURL: srv.URL + "/circle",
Registry: reg,
Limiters: lim,
Client: NewClient(),
})
// Jenkins
eng.Register(&JenkinsSource{
BaseURL: srv.URL + "/jenkins",
Registry: reg,
Limiters: lim,
Client: NewClient(),
})
// GitLab CI
eng.Register(&GitLabCISource{
Token: "glpat-test",
BaseURL: srv.URL + "/gitlabci",
Registry: reg,
Limiters: lim,
Client: NewClient(),
})
// Sanity: all 45 sources registered.
if n := len(eng.List()); n != 45 {
t.Fatalf("expected 45 sources on engine, got %d: %v", n, eng.List())
}
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
@@ -616,6 +687,12 @@ func TestIntegration_AllSources_SweepAll(t *testing.T) {
"recon:k8s",
"recon:terraform",
"recon:helm",
// Phase 14: CI/CD logs
"recon:github_actions",
"recon:travisci",
"recon:circleci",
"recon:jenkins",
"recon:gitlab_ci",
}
for _, st := range wantTypes {
if byType[st] == 0 {
@@ -641,8 +718,8 @@ func TestRegisterAll_Phase12(t *testing.T) {
})
names := eng.List()
if n := len(names); n != 40 {
t.Fatalf("expected 40 sources from RegisterAll, got %d: %v", n, names)
if n := len(names); n != 45 {
t.Fatalf("expected 45 sources from RegisterAll, got %d: %v", n, names)
}
// Build lookup for source access.