Repository: sonatype-nexus-community/nancy (go)

Issue:
Temporary vulnerability exclusion
* **What are you trying to do?**

Temporarily exclude vulnerability checks.

* **What feature or behavior is this required for?**

One of our dependencies has a vulnerability. An upstream fix is forthcoming. We want to temporarily exclude the vulnerability for e.g a month.

* **How could we solve this issue? (Not knowing is okay!)**

For .nancy-ignore perhaps something like:

```
CVE-XYZ # Permanent exclusion
CVE-ABC until=2019-12-01 # Temporary exclusion. On/after 1st December this will be ignored
```

Not sure how this might look for command line arguments.

* **Anything else?**

Happy to work on the code!

cc @bhamail / @DarthHater


Pull request description:
I swear it'll only be for a bit #nancyIgnore
Adds the ability to now make nancy ignores expire after the "until" date has been past. 

Updates the parsing of nancy ignore file to now look for `until=yyyy-MM-dd`. If that is found as long as today is before that date then the ignore will be respected. If its passed that date it will no longer be included in the ignore list.

This has also been based on the pr to add comment support for nancy ignores to just not have to deal with merge issues since its touching all the same nancy ignore parsing code. https://github.com/sonatype-nexus-community/nancy/pull/55

It relates to the following issue #s:
* Fixes #48 

cc @bhamail / @DarthHater


Patch:
diff --git a/.circleci/config.yml b/.circleci/config.yml
index 9d77c54..44e9d35 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -6,6 +6,7 @@ jobs:
     steps:
       - checkout
       - setup_remote_docker
+      - run: docker login -u $DOCKER_USERNAME -p $DOCKER_PASSWORD
       - run: curl -sL https://git.io/goreleaser | bash
   build:
     docker:
diff --git a/.goreleaser.yml b/.goreleaser.yml
index 7c7b68c..92d55b6 100644
--- a/.goreleaser.yml
+++ b/.goreleaser.yml
@@ -20,8 +20,15 @@ builds:
 
 archives:
   - 
+    id: default
     name_template: "{{ .ProjectName }}-{{ .Os }}.{{ .Arch }}-v{{ .Version }}"
     format: binary
+  -
+    id: homebrew
+    name_template: "{{ .ProjectName }}-{{ .Os }}.{{ .Arch }}-v{{ .Version }}"
+    format_overrides:
+      - goos: windows
+        format: zip
 
 checksum:
   name_template: "{{ .ProjectName }}checksums.txt"
@@ -38,10 +45,24 @@ changelog:
     - '^test:'
 
 release:
+  ids:
+    - default
+    - homebrew
   github:
     owner: sonatype-nexus-community
     name: nancy
 
+brews:
+  -
+    ids:
+      - homebrew
+    github:
+      owner: sonatype-nexus-community
+      name: homebrew-nancy-tap
+
+    homepage: https://github.com/sonatype-nexus-community/nancy
+    description: "A tool to check for vulnerabilities in your Golang dependencies, powered by Sonatype OSS Index"
+
 dockers:
   - 
     goos: linux
diff --git a/Dockerfile b/Dockerfile
index 270449e..824a13b 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,4 +1,4 @@
-FROM alpine:latest
+FROM alpine:latest as builder
 
 RUN apk update \
     && apk upgrade \
@@ -8,4 +8,14 @@ RUN apk update \
 
 COPY nancy /
 
+#--------------------------------
+# Deployment Image
+#--------------------------------
+FROM scratch
+
+#Import from builder image
+COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
+COPY --from=builder /etc/passwd /etc/passwd
+COPY --from=builder /nancy /nancy
+
 ENTRYPOINT [ "/nancy" ]
diff --git a/README.md b/README.md
index 23b7a5f..82a3b7b 100644
--- a/README.md
+++ b/README.md
@@ -59,6 +59,17 @@ Options:
 
 `nancy` currently works for projects that use `dep` or `go mod` for dependencies.
 
+#### Homebrew usage
+
+`nancy` can be installed using `brew`:
+
+- `brew tap sonatype-nexus-community/homebrew-nancy-tap`
+- `brew install nancy`
+
+`brew` formulae are created and published to that tap with each new release, so you can use `brew` to upgrade, etc... as you wish.
+
+You can see more about the formulae, etc... at [this repo](https://github.com/sonatype-nexus-community/homebrew-nancy-tap).
+
 #### Docker usage
 
 <p align="center">
@@ -113,6 +124,14 @@ CVN-111
 CVN-123 # Mitigated the risk of this since we only use one method in this package and the affected code doesn't matter
 CVN-543
 ``` 
+It's also possible to define expiring ignores. Meaning that if you define a date on a vulnerability ignore until that date it will be ignored and once that 
+date is passed it will now be reported by nancy if its still an issue. Format to add an expiring ignore looks as follows. They can also be followed up by comments 
+to provide context to as why its been ignored until that date.    
+
+```
+CVN-111 until=2021-01-01
+CVN-543 until=2018-02-12 #Waiting on release from third party. Should be out before this date but gives us a little time to fix it. 
+```
 
 ### Nexus IQ Server Options
 
diff --git a/configuration/parse.go b/configuration/parse.go
index 0d4e295..9949d02 100644
--- a/configuration/parse.go
+++ b/configuration/parse.go
@@ -15,12 +15,14 @@ package configuration
 
 import (
 	"bufio"
+	"errors"
 	"flag"
 	"fmt"
 	"os"
 	"reflect"
 	"regexp"
 	"strings"
+	"time"
 
 	"github.com/sirupsen/logrus"
 	"github.com/sonatype-nexus-community/nancy/audit"
@@ -51,6 +53,7 @@ type IqConfiguration struct {
 }
 
 var unixComments = regexp.MustCompile(`#.*$`)
+var untilComment = regexp.MustCompile(`(until=)(.*)`)
 
 func ParseIQ(args []string) (config IqConfiguration, err error) {
 	iqCommand := flag.NewFlagSet("iq", flag.ExitOnError)
@@ -154,12 +157,10 @@ func getCVEExcludesFromFile(config *Configuration, excludeVulnerabilityFilePath
 
 	scanner := bufio.NewScanner(file)
 	for scanner.Scan() {
-		line := scanner.Text()
-		line = unixComments.ReplaceAllString(line, "")
-		line = strings.TrimSpace(line)
-
-		if len(line) > 0 {
-			config.CveList.Cves = append(config.CveList.Cves, line)
+		ogLine := scanner.Text()
+		err := determineIfLineIsExclusion(ogLine, config)
+		if err != nil {
+			return err
 		}
 	}
 
@@ -169,3 +170,24 @@ func getCVEExcludesFromFile(config *Configuration, excludeVulnerabilityFilePath
 
 	return nil
 }
+
+func determineIfLineIsExclusion(ogLine string, config *Configuration) error {
+	line := unixComments.ReplaceAllString(ogLine, "")
+	until := untilComment.FindStringSubmatch(line)
+	line = untilComment.ReplaceAllString(line, "")
+	cveOnly := strings.TrimSpace(line)
+	if len(cveOnly) > 0 {
+		if until != nil {
+			parseDate, err := time.Parse("2006-01-02", strings.TrimSpace(until[2]))
+			if err != nil {
+				return errors.New(fmt.Sprintf("failed to parse until at line '%s'. Expected format is 'until=yyyy-MM-dd'", ogLine))
+			}
+			if parseDate.After(time.Now()) {
+				config.CveList.Cves = append(config.CveList.Cves, cveOnly)
+			}
+		} else {
+			config.CveList.Cves = append(config.CveList.Cves, cveOnly)
+		}
+	}
+	return nil
+}


Test patch:
diff --git a/configuration/parse_test.go b/configuration/parse_test.go
index 1727ad4..e8916d2 100644
--- a/configuration/parse_test.go
+++ b/configuration/parse_test.go
@@ -19,6 +19,7 @@ import (
 	"flag"
 	"io/ioutil"
 	"os"
+	"strings"
 	"testing"
 
 	"github.com/sonatype-nexus-community/nancy/audit"
@@ -27,55 +28,45 @@ import (
 )
 
 func TestConfigParse(t *testing.T) {
-	file := setupCVEExcludeFile(t, `CVF-000
-CVF-123
-CVF-9999`)
-	emptyFile := setupCVEExcludeFile(t, "")
-	lotsOfRandomNewlinesFile := setupCVEExcludeFile(t, `
+	file, _ := os.Open("../testdata/nancyignores/normalIgnore")
+	emptyFile , _ := os.Open("../testdata/nancyignores/emptyFile")
+	lotsOfRandomNewlinesFile, _ := os.Open("../testdata/nancyignores/lotsOfRandomWhitespace")
+	commentedFile, _ := os.Open("../testdata/nancyignores/commented")
+	untilsFile, _ := os.Open("../testdata/nancyignores/untilsAndComments")
+	invalidUntilsFile, _ := os.Open("../testdata/nancyignores/untilsInvaild")
+	invalidUntilLine, _ := bufio.NewReader(invalidUntilsFile).ReadString('\n')
+	invalidUntilLine = strings.TrimSpace(invalidUntilLine)
+
+	invalidDateUntilsFile, _ := os.Open("../testdata/nancyignores/untilsBadDateFormat")
+	invalidDateUntilLine, _ := bufio.NewReader(invalidDateUntilsFile).ReadString('\n')
+	invalidDateUntilLine = strings.TrimSpace(invalidDateUntilLine)
 
-
-CVN-111
-
-
-
-
-CVN-123
-CVN-543
-`)
-	commentedFile := setupCVEExcludeFile(t, `
-# Comment about this one
-CVN-111 
-CVN-123 #and maybe we put it here too
-# or here
-CVN-543`)
 	dir, _ := ioutil.TempDir("", "prefix")
 
-	defer os.Remove(file.Name())
-	defer os.Remove(emptyFile.Name())
-	defer os.Remove(lotsOfRandomNewlinesFile.Name())
-	defer os.Remove(dir)
-
 	tests := map[string]struct {
 		args           []string
 		expectedConfig Configuration
 		expectedErr    error
 	}{
-		"defaults":                {args: []string{"/tmp/go.sum"}, expectedConfig: Configuration{NoColor: false, Quiet: false, Version: false, CveList: types.CveListFlag{}, Path: "/tmp/go.sum"}, expectedErr: nil},
-		"help":                    {args: []string{"-help", "/tmp/go2.sum"}, expectedConfig: Configuration{Help: true, NoColor: false, Quiet: false, Version: false, CveList: types.CveListFlag{}, Path: "/tmp/go2.sum"}, expectedErr: nil},
-		"no color":                {args: []string{"-no-color", "/tmp/go2.sum"}, expectedConfig: Configuration{NoColor: true, Quiet: false, Version: false, CveList: types.CveListFlag{}, Path: "/tmp/go2.sum"}, expectedErr: nil},
-		"quiet":                   {args: []string{"-quiet", "/tmp/go3.sum"}, expectedConfig: Configuration{NoColor: false, Quiet: true, Version: false, CveList: types.CveListFlag{}, Path: "/tmp/go3.sum"}, expectedErr: nil},
-		"version":                 {args: []string{"-version", "/tmp/go4.sum"}, expectedConfig: Configuration{NoColor: false, Quiet: false, Version: true, CveList: types.CveListFlag{}, Path: "/tmp/go4.sum"}, expectedErr: nil},
-		"exclude vulnerabilities": {args: []string{"-exclude-vulnerability=CVE123,CVE988", "/tmp/go5.sum"}, expectedConfig: Configuration{NoColor: false, Quiet: false, Version: false, CveList: types.CveListFlag{Cves: []string{"CVE123", "CVE988"}}, Path: "/tmp/go5.sum"}, expectedErr: nil},
-		"std in as input":         {args: []string{}, expectedConfig: Configuration{UseStdIn: true}, expectedErr: nil},
-		"path but invalid arg":    {args: []string{"-invalid", "/tmp/go6.sum"}, expectedConfig: Configuration{}, expectedErr: errors.New("flag provided but not defined: -invalid")},
-		"exclude vulnerabilities when has comments":                                  {args: []string{"-exclude-vulnerability-file=" + commentedFile.Name(), "/tmp/go14.sum"}, expectedConfig: Configuration{CveList: types.CveListFlag{Cves: []string{"CVN-111", "CVN-123", "CVN-543"}}, Formatter: &audit.AuditLogTextFormatter{}, Path: "/tmp/go14.sum"}, expectedErr: nil},
-		"exclude vulnerabilities with sane file":                                     {args: []string{"-exclude-vulnerability-file=" + file.Name(), "/tmp/go7.sum"}, expectedConfig: Configuration{CveList: types.CveListFlag{Cves: []string{"CVF-000", "CVF-123", "CVF-9999"}}, Formatter: &audit.AuditLogTextFormatter{}, Path: "/tmp/go7.sum"}, expectedErr: nil},
+		"defaults":                               {args: []string{"/tmp/go.sum"}, expectedConfig: Configuration{NoColor: false, Quiet: false, Version: false, CveList: types.CveListFlag{}, Path: "/tmp/go.sum"}, expectedErr: nil},
+		"help":                                   {args: []string{"-help", "/tmp/go2.sum"}, expectedConfig: Configuration{Help: true, NoColor: false, Quiet: false, Version: false, CveList: types.CveListFlag{}, Path: "/tmp/go2.sum"}, expectedErr: nil},
+		"no color":                               {args: []string{"-no-color", "/tmp/go2.sum"}, expectedConfig: Configuration{NoColor: true, Quiet: false, Version: false, CveList: types.CveListFlag{}, Path: "/tmp/go2.sum"}, expectedErr: nil},
+		"quiet":                                  {args: []string{"-quiet", "/tmp/go3.sum"}, expectedConfig: Configuration{NoColor: false, Quiet: true, Version: false, CveList: types.CveListFlag{}, Path: "/tmp/go3.sum"}, expectedErr: nil},
+		"version":                                {args: []string{"-version", "/tmp/go4.sum"}, expectedConfig: Configuration{NoColor: false, Quiet: false, Version: true, CveList: types.CveListFlag{}, Path: "/tmp/go4.sum"}, expectedErr: nil},
+		"exclude vulnerabilities":                {args: []string{"-exclude-vulnerability=CVE123,CVE988", "/tmp/go5.sum"}, expectedConfig: Configuration{NoColor: false, Quiet: false, Version: false, CveList: types.CveListFlag{Cves: []string{"CVE123", "CVE988"}}, Path: "/tmp/go5.sum"}, expectedErr: nil},
+		"std in as input":                        {args: []string{}, expectedConfig: Configuration{UseStdIn: true}, expectedErr: nil},
+		"path but invalid arg":                   {args: []string{"-invalid", "/tmp/go6.sum"}, expectedConfig: Configuration{}, expectedErr: errors.New("flag provided but not defined: -invalid")},
+		"exclude vulnerabilities with sane file": {args: []string{"-exclude-vulnerability-file=" + file.Name(), "/tmp/go7.sum"}, expectedConfig: Configuration{CveList: types.CveListFlag{Cves: []string{"CVF-000", "CVF-123", "CVF-9999"}}, Formatter: &audit.AuditLogTextFormatter{}, Path: "/tmp/go7.sum"}, expectedErr: nil},
 		"exclude vulnerabilities when file empty":                                    {args: []string{"-exclude-vulnerability-file=" + emptyFile.Name(), "/tmp/go8.sum"}, expectedConfig: Configuration{CveList: types.CveListFlag{}, Formatter: &audit.AuditLogTextFormatter{}, Path: "/tmp/go8.sum"}, expectedErr: nil},
 		"exclude vulnerabilities when file has tons of newlines":                     {args: []string{"-exclude-vulnerability-file=" + lotsOfRandomNewlinesFile.Name(), "/tmp/go9.sum"}, expectedConfig: Configuration{CveList: types.CveListFlag{Cves: []string{"CVN-111", "CVN-123", "CVN-543"}}, Formatter: &audit.AuditLogTextFormatter{}, Path: "/tmp/go9.sum"}, expectedErr: nil},
 		"exclude vulnerabilities are combined with file and args values":             {args: []string{"-exclude-vulnerability=CVE123,CVE988", "-exclude-vulnerability-file=" + lotsOfRandomNewlinesFile.Name(), "/tmp/go10.sum"}, expectedConfig: Configuration{CveList: types.CveListFlag{Cves: []string{"CVE123", "CVE988", "CVN-111", "CVN-123", "CVN-543"}}, Formatter: &audit.AuditLogTextFormatter{}, Path: "/tmp/go10.sum"}, expectedErr: nil},
 		"exclude vulnerabilities file not found doesn't matter":                      {args: []string{"-exclude-vulnerability-file=/blah-blah-doesnt-exists", "/tmp/go11.sum"}, expectedConfig: Configuration{CveList: types.CveListFlag{}, Formatter: &audit.AuditLogTextFormatter{}, Path: "/tmp/go11.sum"}, expectedErr: nil},
 		"exclude vulnerabilities passed as directory doesn't matter":                 {args: []string{"-exclude-vulnerability-file=" + dir, "/tmp/go12.sum"}, expectedConfig: Configuration{CveList: types.CveListFlag{}, Formatter: &audit.AuditLogTextFormatter{}, Path: "/tmp/go12.sum"}, expectedErr: nil},
 		"exclude vulnerabilities doesn't need to be passed if default value is used": {args: []string{"/tmp/go13.sum"}, expectedConfig: Configuration{CveList: types.CveListFlag{Cves: []string{"DEF-111", "DEF-222"}}, Formatter: &audit.AuditLogTextFormatter{}, Path: "/tmp/go13.sum"}, expectedErr: nil},
+		"exclude vulnerabilities when has comments":                                  {args: []string{"-exclude-vulnerability-file=" + commentedFile.Name(), "/tmp/go14.sum"}, expectedConfig: Configuration{CveList: types.CveListFlag{Cves: []string{"CVN-111", "CVN-123", "CVN-543"}}, Path: "/tmp/go14.sum"}, expectedErr: nil},
+		"exclude vulnerabilities when has untils":                                    {args: []string{"-exclude-vulnerability-file=" + untilsFile.Name(), "/tmp/go15.sum"}, expectedConfig: Configuration{CveList: types.CveListFlag{Cves: []string{"NO-UNTIL-888", "MUST-BE-IGNORED-999", "MUST-BE-IGNORED-1999"}}, Path: "/tmp/go15.sum"}, expectedErr: nil},
+		"exclude vulnerabilities when has invalid value in untils":                   {args: []string{"-exclude-vulnerability-file=" + invalidUntilsFile.Name(), "/tmp/go16.sum"}, expectedConfig: Configuration{CveList: types.CveListFlag{}, Path: "/tmp/go16.sum"}, expectedErr: errors.New("failed to parse until at line '" + invalidUntilLine + "'. Expected format is 'until=yyyy-MM-dd'")},
+		"exclude vulnerabilities when has invalid date in untils":                    {args: []string{"-exclude-vulnerability-file=" + invalidDateUntilsFile.Name(), "/tmp/go17.sum"}, expectedConfig: Configuration{CveList: types.CveListFlag{}, Path: "/tmp/go17.sum"}, expectedErr: errors.New("failed to parse until at line '" + invalidDateUntilLine + "'. Expected format is 'until=yyyy-MM-dd'")},
 		"output of json":              {args: []string{"-output=json", "/tmp/go14.sum"}, expectedConfig: Configuration{Formatter: &audit.JsonFormatter{}, Path: "/tmp/go14.sum"}, expectedErr: nil},
 		"output of json pretty print": {args: []string{"-output=json-pretty", "/tmp/go15.sum"}, expectedConfig: Configuration{Formatter: &audit.JsonFormatter{PrettyPrint: true}, Path: "/tmp/go15.sum"}, expectedErr: nil},
 		"output of csv":               {args: []string{"-output=csv", "/tmp/go16.sum"}, expectedConfig: Configuration{Formatter: &audit.CsvFormatter{}, Path: "/tmp/go16.sum"}, expectedErr: nil},
@@ -128,23 +119,6 @@ func TestConfigParseIQ(t *testing.T) {
 	}
 }
 
-func setupCVEExcludeFile(t *testing.T, fileContents string) (file *os.File) {
-	file, err := ioutil.TempFile("", "prefix")
-	if err != nil {
-		t.Fatal(err)
-	}
-	w := bufio.NewWriter(file)
-	_, err = w.WriteString(fileContents)
-	if err != nil {
-		t.Fatal(err)
-	}
-	err = w.Flush()
-	if err != nil {
-		t.Fatal(err)
-	}
-	return file
-}
-
 func setup() {
 	flag.CommandLine = flag.NewFlagSet("", flag.ContinueOnError)
 }
diff --git a/testdata/nancyignores/commented b/testdata/nancyignores/commented
new file mode 100644
index 0000000..c93db49
--- /dev/null
+++ b/testdata/nancyignores/commented
@@ -0,0 +1,5 @@
+# Comment about this one
+CVN-111
+CVN-123 #and maybe we put it here too
+# or here
+CVN-543
diff --git a/testdata/nancyignores/emptyFile b/testdata/nancyignores/emptyFile
new file mode 100644
index 0000000..e69de29
diff --git a/testdata/nancyignores/lotsOfRandomWhitespace b/testdata/nancyignores/lotsOfRandomWhitespace
new file mode 100644
index 0000000..a840bc0
--- /dev/null
+++ b/testdata/nancyignores/lotsOfRandomWhitespace
@@ -0,0 +1,10 @@
+
+
+
+   CVN-111
+
+
+
+
+                CVN-123
+CVN-543
diff --git a/testdata/nancyignores/normalIgnore b/testdata/nancyignores/normalIgnore
new file mode 100644
index 0000000..271d96c
--- /dev/null
+++ b/testdata/nancyignores/normalIgnore
@@ -0,0 +1,3 @@
+CVF-000
+CVF-123
+CVF-9999
diff --git a/testdata/nancyignores/untilsAndComments b/testdata/nancyignores/untilsAndComments
new file mode 100644
index 0000000..a7bfd42
--- /dev/null
+++ b/testdata/nancyignores/untilsAndComments
@@ -0,0 +1,6 @@
+NO-UNTIL-888
+CVN-111      until=2012-12-01     #is anyone looking at these
+CVN-123until=2017-12-01 #wow we are long past this date
+MUST-BE-IGNORED-999 until=2099-12-01 #forever to be excluded from nancy run
+CVN-543                      until=2018-12-01#we should fix this by then
+MUST-BE-IGNORED-1999until=2999-12-01#forever to be excluded from nancy run
diff --git a/testdata/nancyignores/untilsBadDateFormat b/testdata/nancyignores/untilsBadDateFormat
new file mode 100644
index 0000000..04fa4d8
--- /dev/null
+++ b/testdata/nancyignores/untilsBadDateFormat
@@ -0,0 +1,1 @@
+CVN-111 until=12/12/1222
diff --git a/testdata/nancyignores/untilsInvaild b/testdata/nancyignores/untilsInvaild
new file mode 100644
index 0000000..5e69ed6
--- /dev/null
+++ b/testdata/nancyignores/untilsInvaild
@@ -0,0 +1,1 @@
+CVN-111 until=somenonedatevalue