Improved Security Policy Check (#2195)

*  Improved Security Policy Check (#2137)

* Examines and awards points for linked content (URLs / Emails)

* Examines and awards points for hints of disclosure and vulnerability practices

* Examines and awards points for hints of elaboration of timelines

Signed-off-by: Scott Hissam <shissam@gmail.com>

* Repaired Security Policy to correctly use linked content length for evaluation

Signed-off-by: Scott Hissam <shissam@gmail.com>

* gofmt'ed changes

Signed-off-by: Scott Hissam <shissam@gmail.com>

* Repaired the case in the evaluation which was too sensitive to content length over the length of the linked content for urls and emails

Signed-off-by: Scott Hissam <shissam@gmail.com>

* added unit test cases for the new content-based Security Policy checks

Signed-off-by: Scott Hissam <shissam@gmail.com>

* reverted the direct (mistaken) change to checks.md and updated the checks.yaml for generate-docs

Signed-off-by: Scott Hissam <shissam@gmail.com>

*  Improved Security Policy Check (#2137) (revisted based on comments)

* replaced reason strings with log.Info & log.Warn (as seen in --show-details)

* internal assertion check for nil (*pinfo) and empty pfile

* internal switched to FileTypeText over FileTypeSource

* internal implement type SecurityPolicyInformationType/SecurityPolicyInformation revised SecurityPolicyData to support only one file

* revised expected unit-test results and revised unit-test to reflect the new SecurityPolicyData type

Signed-off-by: Scott Hissam <shissam@gmail.com>

* revised the score value based on observation of one *or more* url(s) or one email(s) found; unit tests update accordingly

Signed-off-by: Scott Hissam <shissam@gmail.com>

* revised the score value based on observation of one *or more* url(s) or one email(s) found; unit tests update accordingly

Signed-off-by: Scott Hissam <shissam@gmail.com>

* revised the score value based on observation of one *or more* url(s) or one email(s) found; e2e tests update accordingly

Signed-off-by: Scott Hissam <shissam@gmail.com>

* Addressed PR comments; added telemetry for policy hits in security policy file to track hits by line number

Signed-off-by: Scott Hissam <shissam@gmail.com>

* Resolved merge conflict with checks.yaml

Signed-off-by: Scott Hissam <shissam@gmail.com>

* updated raw results to emit all the raw information for the new security policy check

Signed-off-by: Scott Hissam <shissam@gmail.com>

* Resolved merge conflicts and lint errors with json_raw_results.go

Signed-off-by: Scott Hissam <shissam@gmail.com>

* Addressed review comments to reorganize security policy data struct to support the potential for multiple security policy files.

Signed-off-by: Scott Hissam <shissam@gmail.com>

* Added logic to the security policy to process multiple security policy files only after future improvements to aggregating scoring across such files are designed. For now the security policy behaves as originally designed to stop once one of the expected policy files are found in the repo

Signed-off-by: Scott Hissam <shissam@gmail.com>

* added comments regarding the capacity to support multiple policy files and removed unneeded break statements in the code

Signed-off-by: Scott Hissam <shissam@gmail.com>

* Addressed review comments to remove the dependency on the path in the filename from the code and introduced FileSize to checker.File type and removed the SecurityContentLength which was used to hold that information for the new security policy assessment

Signed-off-by: Scott Hissam <shissam@gmail.com>

* restored reporting full security policy path and filename for policies found in the org level repos

Signed-off-by: Scott Hissam <shissam@gmail.com>

* Resolved conflicts in checks.yaml for documentation

Signed-off-by: Scott Hissam <shissam@gmail.com>

*  CLI for scorecard-attestor (#2309)

* Reorganize

Signed-off-by: Raghav Kaul <raghavkaul@google.com>

* Working commit

Signed-off-by: Raghav Kaul <raghavkaul@google.com>

* Compile with local scorecard; go mod tidy

Signed-off-by: Raghav Kaul <raghavkaul@google.com>

* Add signing code

Heavily borrowed from https://github.com/grafeas/kritis/blob/master/cmd/kritis/signer/main.go

Signed-off-by: Raghav Kaul <raghavkaul@google.com>

* Update deps

* Naming
* Makefile

Signed-off-by: Raghav Kaul <raghavkaul@google.com>

* Edit license, add lint.yml

Signed-off-by: Raghav Kaul <raghavkaul@google.com>

* checks: go mod tidy, license

Signed-off-by: Raghav Kaul <raghavkaul@google.com>

* Address PR comments

* Split into checker/signer files
* Naming convention

Signed-off-by: Raghav Kaul <raghavkaul@google.com>

* License, remove golangci.yml

Signed-off-by: Raghav Kaul <raghavkaul@google.com>

* Address PR comments

* Use cobra

Signed-off-by: Raghav Kaul <raghavkaul@google.com>

* Add tests for root command

Signed-off-by: Raghav Kaul <raghavkaul@google.com>

* Filter out checks that aren't needed for policy evaluation

Signed-off-by: Raghav Kaul <raghavkaul@google.com>

* Add `make` targets for attestor; submit coverage stats

Signed-off-by: Raghav Kaul <raghavkaul@google.com>

* Improvements

* Use sclog instead of glog
* Remove unneeded subcommands
* Formatting

Signed-off-by: Raghav Kaul <raghavkaul@google.com>

* Flags: Make note-name constant and fix messaging

Signed-off-by: Raghav Kaul <raghavkaul@google.com>

* Remove SupportedRequestTypes

Signed-off-by: Raghav Kaul <raghavkaul@google.com>

* go mod tidy

Signed-off-by: Raghav Kaul <raghavkaul@google.com>

* go mod tidy, makefile

Signed-off-by: Raghav Kaul <raghavkaul@google.com>

* Fix GH actions run

Signed-off-by: Raghav Kaul <raghavkaul@google.com>

Signed-off-by: Raghav Kaul <raghavkaul@google.com>
Signed-off-by: Scott Hissam <shissam@gmail.com>

* removed whitespace before stanza for Run attestor e2e

Signed-off-by: Scott Hissam <shissam@gmail.com>

* resolved code review and doc review comments

Signed-off-by: Scott Hissam <shissam@gmail.com>

* repaired the link for the maintainer's guide for supporting the coordinated vulnerability disclosure guidelines

Signed-off-by: Scott Hissam <shissam@gmail.com>

Signed-off-by: Scott Hissam <shissam@gmail.com>
This commit is contained in:
scott hissam 2022-11-04 16:35:44 -05:00 committed by GitHub
parent d67fcbab59
commit 9a85fad9c0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
24 changed files with 517 additions and 64 deletions

View File

@ -76,7 +76,7 @@ jobs:
retry_on: error
timeout_minutes: 30
command: make e2e-pat
- name: Run attestor e2e #using retry because the GitHub token is being throttled.
uses: nick-invision/retry@3e91a01664abd3c5cd539100d10d33b9c5b68482
env:

View File

@ -163,11 +163,37 @@ type VulnerabilitiesData struct {
Vulnerabilities []clients.Vulnerability
}
type SecurityPolicyInformationType string
const (
// forms of security policy hints being evaluated.
SecurityPolicyInformationTypeEmail SecurityPolicyInformationType = "emailAddress"
SecurityPolicyInformationTypeLink SecurityPolicyInformationType = "httpLink"
SecurityPolicyInformationTypeText SecurityPolicyInformationType = "vulnDisclosureText"
)
type SecurityPolicyValueType struct {
Match string // Snippet of match
LineNumber uint // Line number in policy file of match
Offset uint // Offset in the line of the match
}
type SecurityPolicyInformation struct {
InformationType SecurityPolicyInformationType
InformationValue SecurityPolicyValueType
}
type SecurityPolicyFile struct {
// security policy information found in repo or org
Information []SecurityPolicyInformation
// file that contains the security policy information
File File
}
// SecurityPolicyData contains the raw results
// for the Security-Policy check.
type SecurityPolicyData struct {
// Files contains a list of files.
Files []File
PolicyFiles []SecurityPolicyFile
}
// BinaryArtifactData contains the raw results
@ -236,6 +262,7 @@ type File struct {
Snippet string // Snippet of code
Offset uint // Offset in the file of Path (line for source/text files).
EndOffset uint // End of offset in the file, e.g. if the command spans multiple lines.
FileSize uint // Total size of file.
Type FileType // Type of file.
// TODO: add hash.
}

View File

@ -19,6 +19,104 @@ import (
sce "github.com/ossf/scorecard/v4/errors"
)
func scoreSecurityCriteria(f checker.File,
info []checker.SecurityPolicyInformation,
dl checker.DetailLogger,
) int {
var urls, emails, discvuls, linkedContentLen, score int
emails = countSecInfo(info, checker.SecurityPolicyInformationTypeEmail, true)
urls = countSecInfo(info, checker.SecurityPolicyInformationTypeLink, true)
discvuls = countSecInfo(info, checker.SecurityPolicyInformationTypeText, false)
for _, i := range findSecInfo(info, checker.SecurityPolicyInformationTypeEmail, true) {
linkedContentLen += len(i.InformationValue.Match)
}
for _, i := range findSecInfo(info, checker.SecurityPolicyInformationTypeLink, true) {
linkedContentLen += len(i.InformationValue.Match)
}
msg := checker.LogMessage{
Path: f.Path,
Type: f.Type,
Text: "",
}
// #1: linked content found (email/http): score += 6
if (urls + emails) > 0 {
score += 6
msg.Text = "Found linked content in security policy"
dl.Info(&msg)
} else {
msg.Text = "no email or URL found in security policy"
dl.Warn(&msg)
}
// #2: more bytes than the sum of the length of all the linked content found: score += 3
// rationale: there appears to be information and context around those links
// no credit if there is just a link to a site or an email address (those given above)
// the test here is that each piece of linked content will likely contain a space
// before and after the content (hence the two multiplier)
if f.FileSize > 1 && (f.FileSize > uint(linkedContentLen+((urls+emails)*2))) {
score += 3
msg.Text = "Found text in security policy"
dl.Info(&msg)
} else {
msg.Text = "No text (beyond any linked content) found in security policy"
dl.Warn(&msg)
}
// #3: found whole number(s) and or match(es) to "Disclos" and or "Vuln": score += 1
// rationale: works towards the intent of the security policy file
// regarding whom to contact about vuls and disclosures and timing
// e.g., we'll disclose, report a vulnerabily, 30 days, etc.
// looking for at least 2 hits
if discvuls > 1 {
score += 1
msg.Text = "Found disclosure, vulnerability, and/or timelines in security policy"
dl.Info(&msg)
} else {
msg.Text = "One or no descriptive hints of disclosure, vulnerability, and/or timelines in security policy"
dl.Warn(&msg)
}
return score
}
func countSecInfo(secInfo []checker.SecurityPolicyInformation,
infoType checker.SecurityPolicyInformationType,
unique bool,
) int {
keys := make(map[string]bool)
count := 0
for _, entry := range secInfo {
if _, present := keys[entry.InformationValue.Match]; !present && entry.InformationType == infoType {
keys[entry.InformationValue.Match] = true
count += 1
} else if !unique && entry.InformationType == infoType {
count += 1
}
}
return count
}
func findSecInfo(secInfo []checker.SecurityPolicyInformation,
infoType checker.SecurityPolicyInformationType,
unique bool,
) []checker.SecurityPolicyInformation {
keys := make(map[string]bool)
var secList []checker.SecurityPolicyInformation
for _, entry := range secInfo {
if _, present := keys[entry.InformationValue.Match]; !present && entry.InformationType == infoType {
keys[entry.InformationValue.Match] = true
secList = append(secList, entry)
} else if !unique && entry.InformationType == infoType {
secList = append(secList, entry)
}
}
return secList
}
// SecurityPolicy applies the score policy for the Security-Policy check.
func SecurityPolicy(name string, dl checker.DetailLogger, r *checker.SecurityPolicyData) checker.CheckResult {
if r == nil {
@ -27,23 +125,31 @@ func SecurityPolicy(name string, dl checker.DetailLogger, r *checker.SecurityPol
}
// Apply the policy evaluation.
if r.Files == nil || len(r.Files) == 0 {
// If the file is null or has zero lengths, directly return as not detected.
if len(r.PolicyFiles) == 0 {
// If the file is unset, directly return as not detected.
return checker.CreateMinScoreResult(name, "security policy file not detected")
}
for _, f := range r.Files {
// TODO: although this a loop, the raw checks will only return one security policy
// when more than one security policy file can be aggregated into a composite
// score, that logic can be comprehended here.
score := 0
for _, spd := range r.PolicyFiles {
score = scoreSecurityCriteria(spd.File,
spd.Information, dl)
msg := checker.LogMessage{
Path: f.Path,
Type: f.Type,
Offset: f.Offset,
Path: spd.File.Path,
Type: spd.File.Type,
}
if msg.Type == checker.FileTypeURL {
msg.Text = "security policy detected in org repo"
} else {
msg.Text = "security policy detected in current repo"
}
dl.Info(&msg)
}
return checker.CreateMaxScoreResult(name, "security policy file detected")
return checker.CreateResultWithScore(name, "security policy file detected", score)
}

View File

@ -59,16 +59,17 @@ func TestSecurityPolicy(t *testing.T) {
args: args{
name: "test_security_policy_3",
r: &checker.SecurityPolicyData{
Files: []checker.File{
{
PolicyFiles: []checker.SecurityPolicyFile{{
File: checker.File{
Path: "/etc/security/pam_env.conf",
Type: checker.FileTypeURL,
},
Information: make([]checker.SecurityPolicyInformation, 0),
},
},
}},
},
want: checker.CheckResult{
Score: 10,
Score: 0,
},
},
{
@ -76,15 +77,16 @@ func TestSecurityPolicy(t *testing.T) {
args: args{
name: "test_security_policy_4",
r: &checker.SecurityPolicyData{
Files: []checker.File{
{
PolicyFiles: []checker.SecurityPolicyFile{{
File: checker.File{
Path: "/etc/security/pam_env.conf",
},
Information: make([]checker.SecurityPolicyInformation, 0),
},
},
}},
},
want: checker.CheckResult{
Score: 10,
Score: 0,
},
},
}

View File

@ -15,9 +15,11 @@
package raw
import (
"bufio"
"errors"
"fmt"
"path"
"regexp"
"strings"
"github.com/ossf/scorecard/v4/checker"
@ -30,14 +32,14 @@ import (
type securityPolicyFilesWithURI struct {
uri string
files []checker.File
files []checker.SecurityPolicyFile
}
// SecurityPolicy checks for presence of security policy.
// SecurityPolicy checks for presence of security policy
// and applicable content discovered by checkSecurityPolicyFileContent().
func SecurityPolicy(c *checker.CheckRequest) (checker.SecurityPolicyData, error) {
data := securityPolicyFilesWithURI{
uri: "",
files: make([]checker.File, 0),
uri: "", files: make([]checker.SecurityPolicyFile, 0),
}
err := fileparser.OnAllFilesDo(c.RepoClient, isSecurityPolicyFile, &data)
if err != nil {
@ -45,7 +47,16 @@ func SecurityPolicy(c *checker.CheckRequest) (checker.SecurityPolicyData, error)
}
// If we found files in the repo, return immediately.
if len(data.files) > 0 {
return checker.SecurityPolicyData{Files: data.files}, nil
for idx := range data.files {
err := fileparser.OnMatchingFileContentDo(c.RepoClient, fileparser.PathMatcher{
Pattern: data.files[idx].File.Path,
CaseSensitive: false,
}, checkSecurityPolicyFileContent, &data.files[idx].File, &data.files[idx].Information)
if err != nil {
return checker.SecurityPolicyData{}, err
}
}
return checker.SecurityPolicyData{PolicyFiles: data.files}, nil
}
// Check if present in parent org.
@ -70,7 +81,24 @@ func SecurityPolicy(c *checker.CheckRequest) (checker.SecurityPolicyData, error)
}
// Return raw results.
return checker.SecurityPolicyData{Files: data.files}, nil
if len(data.files) > 0 {
for idx := range data.files {
filePattern := data.files[idx].File.Path
// undo path.Join in isSecurityPolicyFile just
// for this call to OnMatchingFileContentsDo
if data.files[idx].File.Type == checker.FileTypeURL {
filePattern = strings.Replace(filePattern, data.uri+"/", "", 1)
}
err := fileparser.OnMatchingFileContentDo(dotGitHubClient, fileparser.PathMatcher{
Pattern: filePattern,
CaseSensitive: false,
}, checkSecurityPolicyFileContent, &data.files[idx].File, &data.files[idx].Information)
if err != nil {
return checker.SecurityPolicyData{}, err
}
}
}
return checker.SecurityPolicyData{PolicyFiles: data.files}, nil
}
// Check repository for repository-specific policy.
@ -85,16 +113,27 @@ var isSecurityPolicyFile fileparser.DoWhileTrueOnFilename = func(name string, ar
}
if isSecurityPolicyFilename(name) {
tempPath := name
tempType := checker.FileTypeSource
tempType := checker.FileTypeText
if pdata.uri != "" {
// report complete path for org-based policy files
tempPath = path.Join(pdata.uri, tempPath)
// FileTypeURL is used in Security-Policy to
// only denote for the details report that the
// policy was found at the org level rather
// than the repo level
tempType = checker.FileTypeURL
}
pdata.files = append(pdata.files, checker.File{
Path: tempPath,
Type: tempType,
Offset: checker.OffsetDefault,
pdata.files = append(pdata.files, checker.SecurityPolicyFile{
File: checker.File{
Path: tempPath,
Type: tempType,
Offset: checker.OffsetDefault,
FileSize: checker.OffsetDefault,
},
Information: make([]checker.SecurityPolicyInformation, 0),
})
// TODO: change 'false' to 'true' when multiple security policy files are supported
// otherwise this check stops at the first security policy found
return false, nil
}
return true, nil
@ -112,3 +151,104 @@ func isSecurityPolicyFilename(name string) bool {
strings.EqualFold(name, "doc/security.rst") ||
strings.EqualFold(name, "docs/security.rst")
}
var checkSecurityPolicyFileContent fileparser.DoWhileTrueOnFileContent = func(path string, content []byte,
args ...interface{},
) (bool, error) {
if len(args) != 2 {
return false, fmt.Errorf(
"checkSecurityPolicyFileContent requires exactly two arguments: %w", errInvalidArgLength)
}
pfiles, ok := args[0].(*checker.File)
if !ok {
return false, fmt.Errorf(
"checkSecurityPolicyFileContent requires argument of type *checker.File: %w", errInvalidArgType)
}
pinfo, ok := args[1].(*[]checker.SecurityPolicyInformation)
if !ok {
return false, fmt.Errorf(
"%s requires argument of type *[]checker.SecurityPolicyInformation: %w",
"checkSecurityPolicyFileContent", errInvalidArgType)
}
if len(content) == 0 {
// perhaps there are more policy files somewhere else,
// keep looking (true)
return true, nil
}
if pfiles != nil && (*pinfo) != nil {
pfiles.Offset = checker.OffsetDefault
pfiles.FileSize = uint(len(content))
policyHits := collectPolicyHits(content)
if len(policyHits) > 0 {
(*pinfo) = append((*pinfo), policyHits...)
}
} else {
e := sce.WithMessage(sce.ErrScorecardInternal, "bad file or information reference")
return false, e
}
// stop here found something, no need to look further (false)
return false, nil
}
func collectPolicyHits(policyContent []byte) []checker.SecurityPolicyInformation {
var hits []checker.SecurityPolicyInformation
// pattern for URLs
reURL := regexp.MustCompile(`(http|https)://[a-zA-Z0-9./?=_%:-]*`)
// pattern for emails
reEML := regexp.MustCompile(`\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,6}\b`)
// pattern for 1 to 4 digit numbers
// or
// strings 'disclos' as in "disclosure" or 'vuln' as in "vulnerability"
reDIG := regexp.MustCompile(`(?i)(\b*[0-9]{1,4}\b|(Disclos|Vuln))`)
lineNum := 0
for {
advance, token, err := bufio.ScanLines(policyContent, true)
if advance == 0 || err != nil {
break
}
lineNum += 1
if len(token) != 0 {
for _, indexes := range reURL.FindAllIndex(token, -1) {
hits = append(hits, checker.SecurityPolicyInformation{
InformationType: checker.SecurityPolicyInformationTypeLink,
InformationValue: checker.SecurityPolicyValueType{
Match: string(token[indexes[0]:indexes[1]]), // Snippet of match
LineNumber: uint(lineNum), // line number in file
Offset: uint(indexes[0]), // Offset in the line
},
})
}
for _, indexes := range reEML.FindAllIndex(token, -1) {
hits = append(hits, checker.SecurityPolicyInformation{
InformationType: checker.SecurityPolicyInformationTypeEmail,
InformationValue: checker.SecurityPolicyValueType{
Match: string(token[indexes[0]:indexes[1]]), // Snippet of match
LineNumber: uint(lineNum), // line number in file
Offset: uint(indexes[0]), // Offset in the line
},
})
}
for _, indexes := range reDIG.FindAllIndex(token, -1) {
hits = append(hits, checker.SecurityPolicyInformation{
InformationType: checker.SecurityPolicyInformationTypeText,
InformationValue: checker.SecurityPolicyValueType{
Match: string(token[indexes[0]:indexes[1]]), // Snippet of match
LineNumber: uint(lineNum), // line number in file
Offset: uint(indexes[0]), // Offset in the line
},
})
}
}
if advance <= len(policyContent) {
policyContent = policyContent[advance:]
}
}
return hits
}

View File

@ -15,6 +15,8 @@
package raw
import (
"fmt"
"os"
"testing"
"github.com/golang/mock/gomock"
@ -65,6 +67,7 @@ func TestSecurityPolicy(t *testing.T) {
tests := []struct {
name string
files []string
path string
result checker.SecurityPolicyData
wantErr bool
want scut.TestReturn
@ -74,30 +77,35 @@ func TestSecurityPolicy(t *testing.T) {
files: []string{
"security.md",
},
path: "",
},
{
name: ".github/security.md",
files: []string{
".github/security.md",
},
path: "",
},
{
name: "docs/security.md",
files: []string{
"docs/security.md",
},
path: "",
},
{
name: "docs/security.rst",
files: []string{
"docs/security.rst",
},
path: "",
},
{
name: "doc/security.rst",
files: []string{
"doc/security.rst",
},
path: "",
},
}
for _, tt := range tests {
@ -110,6 +118,25 @@ func TestSecurityPolicy(t *testing.T) {
mockRepoClient.EXPECT().ListFiles(gomock.Any()).Return(tt.files, nil).AnyTimes()
mockRepo.EXPECT().Org().Return(nil).AnyTimes()
//
// the revised Security Policy will immediate go for the
// file contents once found. This test will return that
// mock file, but this specific unit test is not testing
// for content. As such, this test will crash without
// a mock GetFileContent, so this will return no content
// for the existing file. content test are in overall check
//
mockRepoClient.EXPECT().GetFileContent(gomock.Any()).DoAndReturn(func(fn string) ([]byte, error) {
if tt.path == "" {
return nil, nil
}
content, err := os.ReadFile(tt.path)
if err != nil {
return content, fmt.Errorf("%w", err)
}
return content, nil
}).AnyTimes()
dl := scut.TestDetailLogger{}
c := checker.CheckRequest{
RepoClient: mockRepoClient,
@ -128,8 +155,8 @@ func TestSecurityPolicy(t *testing.T) {
return
}
if len(res.Files) != len(tt.files) {
t.Errorf("test failed: number of files returned is not correct: %+v", res)
if (res.PolicyFiles[0].File.Path) != (tt.files[0]) {
t.Errorf("test failed: the file returned is not correct: %+v", res)
}
})
}

View File

@ -15,6 +15,8 @@
package checks
import (
"fmt"
"os"
"testing"
"github.com/golang/mock/gomock"
@ -29,118 +31,141 @@ func TestSecurityPolicy(t *testing.T) {
//nolint
tests := []struct {
name string
path string
files []string
wantErr bool
want scut.TestReturn
}{
{
name: "security.md",
path: "./testdata/securitypolicy/10_realworld",
files: []string{
"security.md",
},
want: scut.TestReturn{
Score: 10,
NumberOfInfo: 1,
NumberOfInfo: 4,
NumberOfWarn: 0,
},
},
{
name: ".github/security.md",
path: "./testdata/securitypolicy/10_realworldtwo",
files: []string{
".github/security.md",
},
want: scut.TestReturn{
Score: 10,
NumberOfInfo: 1,
NumberOfInfo: 4,
NumberOfWarn: 0,
},
},
{
name: "docs/security.md",
path: "./testdata/securitypolicy/04_textAndDisclosureVuls",
files: []string{
"docs/security.md",
},
want: scut.TestReturn{
Score: 10,
NumberOfInfo: 1,
Score: 4,
NumberOfInfo: 3,
NumberOfWarn: 1,
},
},
{
name: "security.rst",
path: "./testdata/securitypolicy/03_textOnly",
files: []string{
"security.rst",
},
want: scut.TestReturn{
Score: 10,
NumberOfInfo: 1,
Score: 3,
NumberOfInfo: 2,
NumberOfWarn: 2,
},
},
{
name: ".github/security.rst",
path: "./testdata/securitypolicy/06_urlOnly",
files: []string{
".github/security.rst",
},
want: scut.TestReturn{
Score: 10,
NumberOfInfo: 1,
Score: 6,
NumberOfInfo: 2,
NumberOfWarn: 2,
},
},
{
name: "docs/security.rst",
path: "./testdata/securitypolicy/06_emailOnly",
files: []string{
"docs/security.rst",
},
want: scut.TestReturn{
Score: 10,
NumberOfInfo: 1,
Score: 6,
NumberOfInfo: 2,
NumberOfWarn: 2,
},
},
{
name: "doc/security.rst",
path: "./testdata/securitypolicy/06_urlAndEmailOnly",
files: []string{
"doc/security.rst",
},
want: scut.TestReturn{
Score: 10,
NumberOfInfo: 1,
Score: 6,
NumberOfInfo: 2,
NumberOfWarn: 2,
},
},
{
name: "security.adoc",
path: "./testdata/securitypolicy/09_linkedContentAndText",
files: []string{
"security.adoc",
},
want: scut.TestReturn{
Score: 10,
NumberOfInfo: 1,
Score: 9,
NumberOfInfo: 3,
NumberOfWarn: 1,
},
},
{
name: ".github/security.adoc",
path: "./testdata/securitypolicy/10_linkedContentAndTextAndDisclosureVuls",
files: []string{
".github/security.adoc",
},
want: scut.TestReturn{
Score: 10,
NumberOfInfo: 1,
NumberOfInfo: 4,
NumberOfWarn: 0,
},
},
{
name: "docs/security.adoc",
path: "./testdata/securitypolicy/00_empty",
files: []string{
"docs/security.adoc",
},
want: scut.TestReturn{
Score: 10,
Score: 0,
NumberOfInfo: 1,
NumberOfWarn: 3,
},
},
{
name: "Pass Case: Case-insensitive testing",
path: "./testdata/securitypolicy/00_1byte",
files: []string{
"dOCs/SeCuRIty.rsT",
},
want: scut.TestReturn{
Score: 10,
Score: 0,
NumberOfInfo: 1,
NumberOfWarn: 3,
},
},
}
@ -153,6 +178,18 @@ func TestSecurityPolicy(t *testing.T) {
mockRepo := mockrepo.NewMockRepoClient(ctrl)
mockRepo.EXPECT().ListFiles(gomock.Any()).Return(tt.files, nil).AnyTimes()
mockRepo.EXPECT().GetFileContent(gomock.Any()).DoAndReturn(func(fn string) ([]byte, error) {
if tt.path == "" {
return nil, nil
}
content, err := os.ReadFile(tt.path)
if err != nil {
return content, fmt.Errorf("%w", err)
}
return content, nil
}).AnyTimes()
dl := scut.TestDetailLogger{}
c := checker.CheckRequest{
RepoClient: mockRepo,
@ -162,7 +199,7 @@ func TestSecurityPolicy(t *testing.T) {
res := SecurityPolicy(&c)
if !scut.ValidateTestReturn(t, tt.name, &tt.want, &res, &dl) {
t.Errorf("test failed: log message not present: %+v", tt.want)
t.Errorf("test failed: log message not present: %+v on %+v", tt.want, res)
}
})
}

View File

@ -0,0 +1 @@

View File

View File

@ -0,0 +1 @@
now is the time for all goodness for vulnerabilities and disclosures

View File

@ -0,0 +1 @@
now is the time for all goodness

View File

@ -0,0 +1 @@
now is the time for all goodness for vulnerabilities and disclosures

View File

@ -0,0 +1 @@
security@example.com

View File

@ -0,0 +1 @@
https://security.example.com security@example.com

View File

@ -0,0 +1 @@
https://security.example.com

View File

@ -0,0 +1,2 @@
https://security.example.com security@example.com
now is the time for all goodness

View File

@ -0,0 +1,2 @@
https://security.example.com security@example.com
now is the time for all goodness for vulnerabilities and disclosures

View File

@ -0,0 +1,22 @@
# Security Policy
## Security Announcements
Join the [kubernetes-security-announce] group for security and vulnerability announcements.
You can also subscribe to an RSS feed of the above using [this link][kubernetes-security-announce-rss].
## Reporting a Vulnerability
Instructions for reporting a vulnerability can be found on the
[Kubernetes Security and Disclosure Information] page.
## Supported Versions
Information about supported Kubernetes versions can be found on the
[Kubernetes version and version skew support policy] page on the Kubernetes website.
[kubernetes-security-announce]: https://groups.google.com/forum/#!forum/kubernetes-security-announce
[kubernetes-security-announce-rss]: https://groups.google.com/forum/feed/kubernetes-security-announce/msgs/rss_v2_0.xml?num=50
[Kubernetes version and version skew support policy]: https://kubernetes.io/docs/setup/release/version-skew-policy/#supported-versions
[Kubernetes Security and Disclosure Information]: https://kubernetes.io/docs/reference/issues-security/security/#report-a-vulnerability

View File

@ -0,0 +1,11 @@
# Reporting Security Issues
To report a security issue, please email
[oss-security@googlegroups.com](mailto:oss-security@googlegroups.com)
with a description of the issue, the steps you took to create the issue,
affected versions, and, if known, mitigations for the issue.
Our vulnerability management team will respond within 3 working days of your
email. If the issue is confirmed as a vulnerability, we will open a
Security Advisory and acknowledge your contributions as part of it. This project
follows a 90 day disclosure timeline.

View File

@ -169,9 +169,9 @@ func addBinaryArtifactRawResults(r *jsonScorecardRawResult, ba *checker.BinaryAr
//nolint:unparam
func addSecurityPolicyRawResults(r *jsonScorecardRawResult, sp *checker.SecurityPolicyData) error {
r.Results.SecurityPolicies = []jsonFile{}
for _, v := range sp.Files {
for idx := range sp.PolicyFiles {
r.Results.SecurityPolicies = append(r.Results.SecurityPolicies, jsonFile{
Path: v.Path,
Path: sp.PolicyFiles[idx].File.Path,
})
}
return nil

View File

@ -527,11 +527,33 @@ well-known directories.
A security policy (typically a `SECURITY.md` file) can give users information
about what constitutes a vulnerability and how to report one securely so that
information about a bug is not publicly visible.
This check examines the contents of the security policy file awarding points
for those policies that express vulnerability process(es), disclosure timelines,
and have links (e.g., URL(s) and email(s)) to support the users.
Linking Requirements (one or more) (6/10 points):
- A valid form of an email address to contact for vulnerabilities
- A valid form of a http/https address to support vulnerability reporting
Free Form Text (3/10 points):
- Free form text is present in the security policy file which is beyond
simply having a http/https address and/or email in the file
- The string length of any such links in the policy file do not count
towards detecting free form text
Security Policy Specific Text (1/10 points):
- Specific text providing basic or general information about vulnerability
and disclosure practices, expectations, and/or timelines
- Text should include a total of 2 or more hits which match (case insensitive)
`vuln` and as in "Vulnerability" or "vulnerabilities";
`disclos` as "Disclosure" or "disclose";
and numbers which convey expectations of times, e.g., 30 days or 90 days
**Remediation steps**
- Place a security policy file `SECURITY.md` in the root directory of your repository. This makes it easily discoverable by a vulnerability reporter.
- The file should contain information on what constitutes a vulnerability and a way to report it securely (e.g. issue tracker with private issue support, encrypted email with a published public key). Follow the [coordinated vulnerability disclosure guidelines](https://github.com/ossf/oss-vulnerability-guide/blob/main/guide.md) to respond to vulnerability disclosures.
- The file should contain information on what constitutes a vulnerability and a way to report it securely (e.g. issue tracker with private issue support, encrypted email with a published public key). Follow the [coordinated vulnerability disclosure guidelines](https://github.com/ossf/oss-vulnerability-guide/blob/main/maintainer-guide.md) to respond to vulnerability disclosures.
- For GitHub, see more information [here](https://docs.github.com/en/code-security/getting-started/adding-a-security-policy-to-your-repository).
## Signed-Releases

View File

@ -559,6 +559,29 @@ checks:
A security policy (typically a `SECURITY.md` file) can give users information
about what constitutes a vulnerability and how to report one securely so that
information about a bug is not publicly visible.
This check examines the contents of the security policy file awarding points
for those policies that express vulnerability process(es), disclosure timelines,
and have links (e.g., URL(s) and email(s)) to support the users.
Linking Requirements (one or more) (6/10 points):
- A valid form of an email address to contact for vulnerabilities
- A valid form of a http/https address to support vulnerability reporting
Free Form Text (3/10 points):
- Free form text is present in the security policy file which is beyond
simply having a http/https address and/or email in the file
- The string length of any such links in the policy file do not count
towards detecting free form text
Security Policy Specific Text (1/10 points):
- Specific text providing basic or general information about vulnerability
and disclosure practices, expectations, and/or timelines
- Text should include a total of 2 or more hits which match (case insensitive)
`vuln` and as in "Vulnerability" or "vulnerabilities";
`disclos` as "Disclosure" or "disclose";
and numbers which convey expectations of times, e.g., 30 days or 90 days
remediation:
- >-
Place a security policy file `SECURITY.md` in the root directory of your
@ -567,7 +590,7 @@ checks:
The file should contain information on what constitutes a vulnerability
and a way to report it securely (e.g. issue tracker with private issue
support, encrypted email with a published public key). Follow the
[coordinated vulnerability disclosure guidelines](https://github.com/ossf/oss-vulnerability-guide/blob/main/guide.md)
[coordinated vulnerability disclosure guidelines](https://github.com/ossf/oss-vulnerability-guide/blob/main/maintainer-guide.md)
to respond to vulnerability disclosures.
- >-
For GitHub, see more information

View File

@ -49,7 +49,7 @@ var _ = Describe("E2E TEST:"+checks.CheckSecurityPolicy, func() {
Error: nil,
Score: checker.MaxResultScore,
NumberOfWarn: 0,
NumberOfInfo: 1,
NumberOfInfo: 4,
NumberOfDebug: 0,
}
result := checks.SecurityPolicy(&req)
@ -75,7 +75,7 @@ var _ = Describe("E2E TEST:"+checks.CheckSecurityPolicy, func() {
Error: nil,
Score: checker.MaxResultScore,
NumberOfWarn: 0,
NumberOfInfo: 1,
NumberOfInfo: 4,
NumberOfDebug: 0,
}
result := checks.SecurityPolicy(&req)
@ -101,7 +101,7 @@ var _ = Describe("E2E TEST:"+checks.CheckSecurityPolicy, func() {
Error: nil,
Score: checker.MaxResultScore,
NumberOfWarn: 0,
NumberOfInfo: 1,
NumberOfInfo: 4,
NumberOfDebug: 0,
}
result := checks.SecurityPolicy(&req)
@ -127,7 +127,7 @@ var _ = Describe("E2E TEST:"+checks.CheckSecurityPolicy, func() {
Error: nil,
Score: checker.MaxResultScore,
NumberOfWarn: 0,
NumberOfInfo: 1,
NumberOfInfo: 4,
NumberOfDebug: 0,
}
result := checks.SecurityPolicy(&req)
@ -164,7 +164,7 @@ var _ = Describe("E2E TEST:"+checks.CheckSecurityPolicy, func() {
Error: nil,
Score: checker.MaxResultScore,
NumberOfWarn: 0,
NumberOfInfo: 1,
NumberOfInfo: 4,
NumberOfDebug: 0,
}
result := checks.SecurityPolicy(&req)

View File

@ -221,6 +221,19 @@ type jsonTokenPermission struct {
Type string `json:"type"`
}
type jsonSecurityFile struct {
Path string `json:"path"`
Hits []jsonSecurityPolicyHits `json:"matches,omitempty"`
ContentLength uint `json:"contentLength,omitempty"`
}
type jsonSecurityPolicyHits struct {
Type string `json:"type"`
Match string `json:"match,omitempty"`
LineNumber uint `json:"lineNumber,omitempty"`
Offset uint `json:"offset,omitempty"`
}
//nolint:govet
type jsonRawResults struct {
// Workflow results.
@ -239,7 +252,7 @@ type jsonRawResults struct {
Binaries []jsonFile `json:"binaries"`
// List of security policy files found in the repo.
// Note: we return one at most.
SecurityPolicies []jsonFile `json:"securityPolicies"`
SecurityPolicies []jsonSecurityFile `json:"securityPolicies"`
// List of update tools.
// Note: we return one at most.
DependencyUpdateTools []jsonTool `json:"dependencyUpdateTools"`
@ -587,11 +600,23 @@ func (r *jsonScorecardRawResult) addBinaryArtifactRawResults(ba *checker.BinaryA
//nolint:unparam
func (r *jsonScorecardRawResult) addSecurityPolicyRawResults(sp *checker.SecurityPolicyData) error {
r.Results.SecurityPolicies = []jsonFile{}
for _, v := range sp.Files {
r.Results.SecurityPolicies = append(r.Results.SecurityPolicies, jsonFile{
Path: v.Path,
})
r.Results.SecurityPolicies = []jsonSecurityFile{}
if len(sp.PolicyFiles) > 0 {
for idx := range sp.PolicyFiles {
r.Results.SecurityPolicies = append(r.Results.SecurityPolicies, jsonSecurityFile{
Path: sp.PolicyFiles[idx].File.Path,
ContentLength: sp.PolicyFiles[idx].File.FileSize,
Hits: []jsonSecurityPolicyHits{},
})
for _, entry := range sp.PolicyFiles[idx].Information {
r.Results.SecurityPolicies[idx].Hits = append(r.Results.SecurityPolicies[idx].Hits, jsonSecurityPolicyHits{
Type: string(entry.InformationType),
Match: entry.InformationValue.Match,
LineNumber: entry.InformationValue.LineNumber,
Offset: entry.InformationValue.Offset,
})
}
}
}
return nil
}