176 lines
4.7 KiB
Go
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 ""
|
|
}
|