Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -272,6 +272,10 @@ detects a repository to be out of compliance.
(not currently user configurable). If the policy result changes, a new comment
will be left on the issue and linked in the issue body. Once the violation is
addressed, the issue will be automatically closed by Allstar within 5-10 minutes.
- `vulnerability_report`: This action creates a GitHub private vulnerability
report for the repository. It is intended for policy failures that may be
exploitable and should not be opened as public issues. Allstar checks for an
existing report with the same policy summary before creating a new one.
- `fix`: This action is policy specific. The policy will make the changes to the
GitHub settings to correct the policy violation. Not all policies will be able
to support this (see below).
Expand Down
5 changes: 5 additions & 0 deletions operator.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,11 @@ to create a new app.
> repository permission set to **Read & write**. This is required for uploading
> SARIF results to the Code Scanning API.

> **Note:** If you plan to use the `vulnerability_report` action, your GitHub
> App also needs the **Repository security advisories** repository permission set
> to **Read & write**, and target repositories must support private vulnerability
> reporting.

> **Note:** As Allstar is developed, it may evolve the permissions needed or start
> listening for webhooks, please follow along development in this repo.

Expand Down
28 changes: 18 additions & 10 deletions pkg/enforce/enforce.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import (
"github.com/ossf/allstar/pkg/policies"
"github.com/ossf/allstar/pkg/policydef"
"github.com/ossf/allstar/pkg/scorecard"
"github.com/ossf/allstar/pkg/vulnerabilityreport"
)

type (
Expand All @@ -42,22 +43,24 @@ type (
)

var (
doNothingOnOptOut = operator.DoNothingOnOptOut
policiesGetPolicies func() []policydef.Policy
issueEnsure func(context.Context, *github.Client, string, string, string, string) error
issueClose func(context.Context, *github.Client, string, string, string) error
configIsBotEnabled func(context.Context, *github.Client, string, string) bool
getAppInstallations func(context.Context, *github.Client) ([]*github.Installation, error)
getAppInstallationRepos func(context.Context, *github.Client) ([]*github.Repository, *github.Response, error)
runPolicies func(context.Context, *github.Client, string, string, bool, string) (EnforceRepoResults, error)
deleteInstallation func(context.Context, *github.Client, int64) (*github.Response, error)
listInstallations func(context.Context, *github.Client) ([]*github.Installation, error)
doNothingOnOptOut = operator.DoNothingOnOptOut
policiesGetPolicies func() []policydef.Policy
issueEnsure func(context.Context, *github.Client, string, string, string, string) error
issueClose func(context.Context, *github.Client, string, string, string) error
vulnerabilityReportEnsure func(context.Context, *github.Client, string, string, string, string) error
configIsBotEnabled func(context.Context, *github.Client, string, string) bool
getAppInstallations func(context.Context, *github.Client) ([]*github.Installation, error)
getAppInstallationRepos func(context.Context, *github.Client) ([]*github.Repository, *github.Response, error)
runPolicies func(context.Context, *github.Client, string, string, bool, string) (EnforceRepoResults, error)
deleteInstallation func(context.Context, *github.Client, int64) (*github.Response, error)
listInstallations func(context.Context, *github.Client) ([]*github.Installation, error)
)

func init() {
policiesGetPolicies = policies.GetPolicies
issueEnsure = issue.Ensure
issueClose = issue.Close
vulnerabilityReportEnsure = vulnerabilityreport.Ensure
configIsBotEnabled = config.IsBotEnabled
getAppInstallations = getAppInstallationsReal
getAppInstallationRepos = getAppInstallationReposReal
Expand Down Expand Up @@ -385,6 +388,11 @@ func runPoliciesReal(ctx context.Context, c *github.Client, owner, repo string,
if err != nil {
return nil, err
}
case "vulnerability_report":
err := vulnerabilityReportEnsure(ctx, c, owner, repo, p.Name(), r.NotifyText)
if err != nil {
return nil, err
}
case "email":
log.Warn().
Str("org", owner).
Expand Down
29 changes: 29 additions & 0 deletions pkg/enforce/enforce_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,11 @@ func TestRunPolicies(t *testing.T) {
ensureCalled = true
return nil
}
reportCalled := false
vulnerabilityReportEnsure = func(ctx context.Context, c *github.Client, owner, repo, policy, text string) error {
reportCalled = true
return nil
}
closeCalled := false
issueClose = func(ctx context.Context, c *github.Client, owner, repo, policy string) error {
closeCalled = true
Expand All @@ -117,6 +122,7 @@ func TestRunPolicies(t *testing.T) {
Action string
ShouldFix bool
ShouldEnsure bool
ShouldReport bool
ShouldClose bool
ExpEnforceResults EnforceRepoResults
}{
Expand All @@ -141,6 +147,21 @@ func TestRunPolicies(t *testing.T) {
Action: "issue",
ShouldFix: false,
ShouldEnsure: true,
ShouldReport: false,
ShouldClose: false,
ExpEnforceResults: EnforceRepoResults{
"Test policy": false,
},
},
{
Name: "OpenVulnerabilityReport",
Res: policyRepoResults{
"fake-repo": policydef.Result{Enabled: true, Pass: false},
},
Action: "vulnerability_report",
ShouldFix: false,
ShouldEnsure: false,
ShouldReport: true,
ShouldClose: false,
ExpEnforceResults: EnforceRepoResults{
"Test policy": false,
Expand Down Expand Up @@ -201,6 +222,7 @@ func TestRunPolicies(t *testing.T) {
t.Run(test.Name, func(t *testing.T) {
fixCalled = false
ensureCalled = false
reportCalled = false
closeCalled = false
policy1Results = test.Res
action = test.Action
Expand All @@ -223,6 +245,13 @@ func TestRunPolicies(t *testing.T) {
t.Error("Ensure called unexpectedly.")
}
}
if test.ShouldReport != reportCalled {
if test.ShouldReport {
t.Error("Expected vulnerability report Ensure to be called")
} else {
t.Error("Vulnerability report Ensure called unexpectedly.")
}
}
if test.ShouldClose != closeCalled {
if test.ShouldClose {
t.Error("Expected Close to be called")
Expand Down
164 changes: 164 additions & 0 deletions pkg/vulnerabilityreport/vulnerabilityreport.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
// Copyright 2026 Allstar Authors

// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at

// http://www.apache.org/licenses/LICENSE-2.0

// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

// Package vulnerabilityreport handles creating private vulnerability reports for
// Allstar policy failures.
package vulnerabilityreport

import (
"context"
"errors"
"fmt"
"net/http"

"github.com/google/go-github/v84/github"
"github.com/rs/zerolog/log"
)

const (
reportTitleFormat = "Allstar Security Policy violation: %s"

defaultSeverity = "high"
)

type securityAdvisories interface {
ListRepositorySecurityAdvisories(context.Context, string, string, *github.ListRepositorySecurityAdvisoriesOptions) (
[]*github.SecurityAdvisory, *github.Response, error)
}

type createPrivateVulnerabilityReportRequest struct {
Summary *string `json:"summary,omitempty"`
Description *string `json:"description,omitempty"`
Severity *string `json:"severity,omitempty"`
}

var createReport = createReportReal

// Ensure ensures a private vulnerability report exists for the provided repo
// and policy. Existing reports are matched by summary to avoid creating a new
// private report on every enforcement run.
func Ensure(ctx context.Context, c *github.Client, owner, repo, policy, text string) error {
return ensure(ctx, c, c.SecurityAdvisories, owner, repo, policy, text)
}

func ensure(ctx context.Context, c *github.Client, advisories securityAdvisories, owner, repo, policy, text string) error {
summary := fmt.Sprintf(reportTitleFormat, policy)
existing, err := getPolicyReport(ctx, advisories, owner, repo, summary)
if err != nil {
if reportUnavailable(nil, err) {
logUnavailable(owner, repo, policy, err)
return nil
}
return err
}
if existing != nil {
log.Info().
Str("org", owner).
Str("repo", repo).
Str("area", policy).
Str("report", existing.GetHTMLURL()).
Msg("Private vulnerability report already exists.")
return nil
}

description := createReportDescription(text)
report, rsp, err := createReport(ctx, c, owner, repo, summary, description)
if err != nil && reportUnavailable(rsp, err) {
logUnavailable(owner, repo, policy, err)
return nil
}
if err != nil {
return err
}
log.Info().
Str("org", owner).
Str("repo", repo).
Str("area", policy).
Str("report", report.GetHTMLURL()).
Msg("Created private vulnerability report.")
return nil
}

func getPolicyReport(ctx context.Context, svc securityAdvisories, owner, repo, summary string) (*github.SecurityAdvisory, error) {
opt := &github.ListRepositorySecurityAdvisoriesOptions{
ListCursorOptions: github.ListCursorOptions{
PerPage: 100,
},
}
for {
advisories, resp, err := svc.ListRepositorySecurityAdvisories(ctx, owner, repo, opt)
if err != nil {
return nil, err
}
for _, advisory := range advisories {
if advisory.GetSummary() == summary {
return advisory, nil
}
}
if resp == nil || resp.After == "" {
break
}
opt.After = resp.After
}
return nil, nil
}

func createReportReal(ctx context.Context, c *github.Client, owner, repo, summary, description string) (
*github.SecurityAdvisory, *github.Response, error,
) {
url := fmt.Sprintf("repos/%v/%v/security-advisories/reports", owner, repo)
req, err := c.NewRequest("POST", url, &createPrivateVulnerabilityReportRequest{
Summary: github.Ptr(summary),
Description: github.Ptr(description),
Severity: github.Ptr(defaultSeverity),
})
if err != nil {
return nil, nil, err
}

report := new(github.SecurityAdvisory)
resp, err := c.Do(ctx, req, report)
if err != nil {
return nil, resp, err
}
return report, resp, nil
}

func reportUnavailable(rsp *github.Response, err error) bool {
if rsp != nil && (rsp.StatusCode == http.StatusForbidden || rsp.StatusCode == http.StatusNotFound) {
return true
}
var ghErr *github.ErrorResponse
if errors.As(err, &ghErr) && ghErr.Response != nil {
return ghErr.Response.StatusCode == http.StatusForbidden || ghErr.Response.StatusCode == http.StatusNotFound
}
return false
}

func logUnavailable(owner, repo, policy string, err error) {
log.Warn().
Str("org", owner).
Str("repo", repo).
Str("area", policy).
Err(err).
Msg("Action set to vulnerability_report, but private vulnerability reporting is unavailable.")
}

func createReportDescription(text string) string {
return fmt.Sprintf(`Allstar detected a security policy violation.

%s

This private vulnerability report was created automatically so maintainers can triage potentially exploitable workflow or repository security issues without opening a public issue first.`, text)
}
Loading