-
Notifications
You must be signed in to change notification settings - Fork 18
feat: add AI evaluation for SA-01.01, SA-02.01, SA-03.01, SA-03.02 #480
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,165 @@ | ||
| package data | ||
|
|
||
| import ( | ||
| "context" | ||
| "encoding/base64" | ||
| "errors" | ||
| "fmt" | ||
| "io" | ||
| "net/http" | ||
| "net/url" | ||
| "path" | ||
| "strings" | ||
| "unicode" | ||
| "unicode/utf8" | ||
|
|
||
| "github.com/google/go-github/v74/github" | ||
| ) | ||
|
|
||
| const maxDeclaredDocumentationBytes = 64 * 1024 | ||
|
|
||
| type declaredDocumentationResult struct { | ||
| file DocumentationFile | ||
| err error | ||
| } | ||
|
|
||
| // GetDeclaredDocumentation reads only the same-repository text artifact named by | ||
| // a Security Insights URL, at its declared ref. Results, including errors and | ||
| // empty documents, are shared across steps through the payload cache. | ||
| func (p *Payload) GetDeclaredDocumentation(rawURL string) (DocumentationFile, error) { | ||
| if p == nil { | ||
| return DocumentationFile{}, errors.New("payload missing required repository data") | ||
| } | ||
| if p.cache == nil { | ||
| p.cache = &payloadCache{} | ||
| } | ||
| if result, ok := p.cache.declaredDocumentation[rawURL]; ok { | ||
| return result.file, result.err | ||
| } | ||
| file, err := p.getDeclaredDocumentation(rawURL) | ||
| if p.cache.declaredDocumentation == nil { | ||
| p.cache.declaredDocumentation = make(map[string]declaredDocumentationResult) | ||
| } | ||
| p.cache.declaredDocumentation[rawURL] = declaredDocumentationResult{file: file, err: err} | ||
| return file, err | ||
| } | ||
|
|
||
| func (r *RestData) getDeclaredDocumentation(rawURL string) (DocumentationFile, error) { | ||
| if r == nil || r.owner == "" || r.repo == "" { | ||
| return DocumentationFile{}, errors.New("payload missing required repository identity") | ||
| } | ||
| ref, filePath, err := parseDeclaredDocumentationURL(rawURL, r.owner, r.repo) | ||
| if err != nil { | ||
| return DocumentationFile{}, err | ||
| } | ||
| if r.ghClient == nil { | ||
| return DocumentationFile{}, errors.New("payload missing GitHub API client") | ||
| } | ||
|
|
||
| // Reuse the configured API transport (including authentication and counting), | ||
| // but never follow redirects: an OAuth transport can attach credentials again | ||
| // even when net/http strips Authorization on a cross-host redirect. | ||
| httpClient := *r.ghClient.Client() | ||
| httpClient.CheckRedirect = func(*http.Request, []*http.Request) error { | ||
| return http.ErrUseLastResponse | ||
| } | ||
| client := github.NewClient(&httpClient) | ||
| client.BaseURL = r.ghClient.BaseURL | ||
| entry, _, _, err := client.Repositories.GetContents(context.Background(), r.owner, r.repo, filePath, &github.RepositoryContentGetOptions{Ref: ref}) | ||
| if err != nil { | ||
| return DocumentationFile{}, fmt.Errorf("read declared documentation %s at ref %s: %w", filePath, ref, err) | ||
| } | ||
| if entry == nil || entry.GetType() != "file" || entry.GetTarget() != "" || entry.GetSubmoduleGitURL() != "" { | ||
| return DocumentationFile{}, errors.New("declared documentation is not a regular file") | ||
| } | ||
| if entry.GetPath() != filePath { | ||
| return DocumentationFile{}, errors.New("declared documentation response path does not match requested path") | ||
| } | ||
| if entry.GetSize() < 0 { | ||
| return DocumentationFile{}, errors.New("declared documentation has an invalid file size") | ||
| } | ||
| if entry.GetSize() > maxDeclaredDocumentationBytes { | ||
| return DocumentationFile{}, fmt.Errorf("declared documentation exceeds %d bytes", maxDeclaredDocumentationBytes) | ||
| } | ||
| if entry.Content == nil || entry.GetEncoding() != "base64" { | ||
| return DocumentationFile{}, errors.New("declared documentation has missing content or unsupported encoding") | ||
| } | ||
| decoded, err := io.ReadAll(io.LimitReader( | ||
| base64.NewDecoder(base64.StdEncoding, strings.NewReader(*entry.Content)), | ||
| maxDeclaredDocumentationBytes+1, | ||
| )) | ||
| if err != nil { | ||
| return DocumentationFile{}, fmt.Errorf("decode declared documentation: %w", err) | ||
| } | ||
| if len(decoded) > maxDeclaredDocumentationBytes { | ||
| return DocumentationFile{}, fmt.Errorf("declared documentation exceeds %d bytes", maxDeclaredDocumentationBytes) | ||
| } | ||
| text := string(decoded) | ||
| if !utf8.ValidString(text) || strings.ContainsFunc(text, func(r rune) bool { | ||
| return unicode.IsControl(r) && r != '\n' && r != '\r' && r != '\t' | ||
| }) || (len(decoded) > 0 && !strings.HasPrefix(http.DetectContentType(decoded), "text/")) { | ||
| return DocumentationFile{}, errors.New("declared documentation is not UTF-8 text") | ||
| } | ||
| return DocumentationFile{Path: filePath, Content: text}, nil | ||
| } | ||
|
|
||
| func parseDeclaredDocumentationURL(rawURL, owner, repo string) (ref, filePath string, err error) { | ||
| u, err := url.Parse(rawURL) | ||
| if err != nil { | ||
| return "", "", errors.New("invalid declared documentation URL") | ||
| } | ||
| if u.Scheme != "https" || u.User != nil || u.RawQuery != "" || u.ForceQuery || u.Opaque != "" { | ||
| return "", "", errors.New("declared documentation URL must use HTTPS without userinfo or query strings") | ||
| } | ||
| host := strings.ToLower(u.Host) | ||
| if host != "github.com" && host != "raw.githubusercontent.com" { | ||
| return "", "", errors.New("declared documentation URL must use github.com or raw.githubusercontent.com") | ||
| } | ||
| // Split before unescaping so a slash-containing ref stays one URL segment. | ||
| segments := strings.Split(strings.TrimPrefix(u.EscapedPath(), "/"), "/") | ||
| refIndex := 2 | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🤖 (medium) GitHub's Raw button now emits |
||
| if host == "github.com" { | ||
| refIndex = 3 | ||
| } | ||
| if len(segments) < refIndex+2 { | ||
| return "", "", errors.New("declared documentation URL is missing a ref or file path") | ||
| } | ||
| for i, segment := range segments { | ||
| decoded, decodeErr := url.PathUnescape(segment) | ||
| if decodeErr != nil || !validDeclaredDocumentationSegment(decoded, i == refIndex) { | ||
| return "", "", errors.New("declared documentation URL contains an invalid path segment") | ||
| } | ||
| segments[i] = decoded | ||
| } | ||
| if !strings.EqualFold(segments[0], owner) || !strings.EqualFold(segments[1], repo) { | ||
| return "", "", errors.New("declared documentation URL must reference the assessed repository") | ||
| } | ||
| if host == "github.com" && segments[2] != "blob" { | ||
| return "", "", errors.New("declared documentation GitHub URL must use /blob/<ref>/<path>") | ||
| } | ||
| ref = segments[refIndex] | ||
| filePath = strings.Join(segments[refIndex+1:], "/") | ||
| switch strings.ToLower(path.Ext(filePath)) { | ||
| case ".json", ".yaml", ".yml", ".proto": | ||
| default: | ||
| if !isDocumentationPath(filePath) { | ||
| return "", "", errors.New("declared documentation has an unsupported file extension") | ||
| } | ||
| } | ||
| return ref, filePath, nil | ||
| } | ||
|
|
||
| func validDeclaredDocumentationSegment(segment string, isRef bool) bool { | ||
| if !utf8.ValidString(segment) || strings.ContainsAny(segment, "\\%") || strings.ContainsFunc(segment, unicode.IsControl) { | ||
| return false | ||
| } | ||
| if !isRef && strings.Contains(segment, "/") { | ||
| return false | ||
| } | ||
| for _, part := range strings.Split(segment, "/") { | ||
| if part == "" || part == "." || part == ".." { | ||
| return false | ||
| } | ||
| } | ||
| return true | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🤖 (medium)
unicode.IsControlreturns false for every rune above U+00FF, so this filter admits the entire Cf category: zero-width spaces, bidi overrides, and the U+E0000 tag block used for hidden-text prompt injection. Fetched content is fully controlled by the scanned repo, and for SA-01.01/SA-03.01/SA-03.02 a model pass becomes Passed directly (at High confidence for SA-03.x, since the cap only covers design).Also worth considering extending the confidence cap or pass deferral to the SA-03.x behaviors.