// SPDX-License-Identifier: EUPL-1.2 package security import ( "net/http" "dappco.re/go/core" "forge.lthn.ai/core/cli/pkg/cli" ) const githubAPIBase = "https://api.github.com" func printJSON(value any) error { output := core.JSONMarshal(value) if !output.OK { return cli.Wrap(output.Value.(error), "marshal JSON output") } cli.Text(string(output.Value.([]byte))) return nil } func githubToken() string { if token := core.Env("GH_TOKEN"); token != "" { return token } return core.Env("GITHUB_TOKEN") } // checkGH verifies GitHub API authentication is configured. func checkGH() error { if githubToken() == "" { return core.E("security.checkGH", "missing GitHub token (set GH_TOKEN or GITHUB_TOKEN)", nil) } return nil } // runGHAPI fetches all paginated GitHub API results for an array endpoint. func runGHAPI(endpoint string) ([]byte, error) { pages, err := runGHAPIPages(endpoint) if err != nil { return nil, err } return []byte(combineGitHubPages(pages)), nil } func runGHAPIPages(endpoint string) ([]string, error) { nextURL := core.Concat(githubAPIBase, "/", endpoint) pages := make([]string, 0, 1) for nextURL != "" { status, body, headers, err := executeGitHubRequest(http.MethodGet, nextURL, "") if err != nil { return nil, err } switch status { case http.StatusOK: case http.StatusNotFound: return nil, nil case http.StatusForbidden: return nil, core.E("security.runGHAPI", "access denied (check token permissions)", nil) default: msg := core.Sprintf("GitHub API GET %d", status) if trimmed := core.Trim(body); trimmed != "" { msg = core.Concat(msg, ": ", trimmed) } return nil, core.E("security.runGHAPI", msg, nil) } pages = append(pages, body) nextURL = nextGitHubPage(headers.Get("Link")) } return pages, nil } func createGitHubIssue(repo, title, body string, labels []string) (string, error) { payload := core.JSONMarshal(map[string]any{ "title": title, "body": body, "labels": labels, }) if !payload.OK { return "", core.E("security.createGitHubIssue", "marshal issue request", payload.Value.(error)) } status, responseBody, _, err := executeGitHubRequest( http.MethodPost, core.Concat(githubAPIBase, "/repos/", repo, "/issues"), string(payload.Value.([]byte)), ) if err != nil { return "", err } if status < http.StatusOK || status >= http.StatusMultipleChoices { msg := core.Sprintf("GitHub issue create %d", status) if trimmed := core.Trim(responseBody); trimmed != "" { msg = core.Concat(msg, ": ", trimmed) } return "", core.E("security.createGitHubIssue", msg, nil) } var response struct { HTMLURL string `json:"html_url"` } if decoded := core.JSONUnmarshalString(responseBody, &response); !decoded.OK { return "", core.E("security.createGitHubIssue", "decode issue response", decoded.Value.(error)) } if response.HTMLURL == "" { return "", core.E("security.createGitHubIssue", "issue response missing html_url", nil) } return response.HTMLURL, nil } func executeGitHubRequest(method, targetURL, body string) (int, string, http.Header, error) { req, err := http.NewRequest(method, targetURL, core.NewReader(body)) if err != nil { return 0, "", nil, core.E("security.executeGitHubRequest", "build request", err) } req.Header.Set("Accept", "application/vnd.github+json") req.Header.Set("Content-Type", "application/json") req.Header.Set("User-Agent", "core-ai-security") req.Header.Set("X-GitHub-Api-Version", "2022-11-28") if token := githubToken(); token != "" { req.Header.Set("Authorization", core.Concat("Bearer ", token)) } resp, err := http.DefaultClient.Do(req) if err != nil { return 0, "", nil, core.E("security.executeGitHubRequest", "send request", err) } payload := core.ReadAll(resp.Body) if !payload.OK { return 0, "", nil, core.E("security.executeGitHubRequest", "read response body", payload.Value.(error)) } return resp.StatusCode, payload.Value.(string), resp.Header, nil } func combineGitHubPages(pages []string) string { parts := make([]string, 0, len(pages)) for _, page := range pages { trimmed := core.Trim(page) if trimmed == "" || trimmed == "[]" { continue } trimmed = core.TrimPrefix(trimmed, "[") trimmed = core.TrimSuffix(trimmed, "]") trimmed = core.Trim(trimmed) if trimmed != "" { parts = append(parts, trimmed) } } if len(parts) == 0 { return "[]" } return core.Concat("[", core.Join(",", parts...), "]") } func nextGitHubPage(linkHeader string) string { for _, part := range core.Split(linkHeader, ",") { if !core.Contains(part, `rel="next"`) { continue } urlPart := core.Trim(core.SplitN(part, ";", 2)[0]) urlPart = core.TrimPrefix(urlPart, "<") return core.TrimSuffix(urlPart, ">") } return "" }