go-ai/cmd/security/github.go
Virgil 5cf7f37444 refactor(core): complete v0.8.0 polish pass
Co-Authored-By: Virgil <virgil@lethean.io>
2026-03-26 16:41:10 +00:00

176 lines
4.7 KiB
Go

// 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 ""
}