Skip to content
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

Support disabling deletion of certain CVEs #100

Merged
merged 2 commits into from
Feb 23, 2022
Merged
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
8 changes: 8 additions & 0 deletions changelog/@unreleased/pr-100.v2.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
type: improvement
improvement:
description: |-
Support disabling deletion of certain CVEs

`--disable-cve-2021-45105-detection` and `--disable-cve-2021-44832-detection` flags have been added to the delete command to allow for deleting only findings that map to certain CVEs. Some vulnerable files will contain multiple CVEs and so it is advised that the desired combination of `--disable-cve-*` flags be found by running with `--dry-run=true` (which is the default value) first.
links:
- https://github.com/palantir/log4j-sniffer/pull/100
13 changes: 5 additions & 8 deletions cmd/crawl.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,8 @@ import (
func crawlCmd() *cobra.Command {
var (
cmdCrawlFlags crawlFlags
cmdCVEFlags cveFlags
disableDetailedFindings bool
disableCVE45105 bool
disableCVE44832 bool
disableFlaggingJndiLookup bool
disableUnknownVersions bool
outputJSON bool
Expand All @@ -55,8 +54,7 @@ Use the ignore-dir flag to provide directories of which to ignore all nested fil
OutputJSON: outputJSON,
OutputFilePathOnly: outputFilePathOnly,
OutputWriter: cmd.OutOrStdout(),
DisableCVE45105: disableCVE45105,
DisableCVE44832: disableCVE44832,
CVEResolver: cmdCVEFlags.cveResolver(),
DisableFlaggingJndiLookup: disableFlaggingJndiLookup,
DisableFlaggingUnknownVersions: disableUnknownVersions,
}
Expand Down Expand Up @@ -92,10 +90,10 @@ Use the ignore-dir flag to provide directories of which to ignore all nested fil
} else {
var cveInfo string
cveInfo = "CVE-2021-44228 or CVE-2021-45046"
if !disableCVE45105 {
if !cmdCVEFlags.disableCVE45105 {
cveInfo += " or CVE-2021-45105"
}
if !disableCVE44832 {
if !cmdCVEFlags.disableCVE44832 {
cveInfo += " or CVE-2021-44832"
}
count := reporter.FileCount()
Expand All @@ -114,11 +112,10 @@ Use the ignore-dir flag to provide directories of which to ignore all nested fil
}

applyCrawlFlags(&cmd, &cmdCrawlFlags)
applyCVEFlags(&cmd, &cmdCVEFlags)
cmd.Flags().BoolVar(&disableDetailedFindings, "disable-detailed-findings", false, "Do not print out detailed finding information when not outputting in JSON.")
cmd.Flags().BoolVar(&disableFlaggingJndiLookup, "disable-flagging-jndi-lookup", false, `Do not report results that only match on the presence of a JndiLookup class.
Even when disabled results which match other criteria will still report the presence of JndiLookup if relevant.`)
cmd.Flags().BoolVar(&disableCVE45105, "disable-cve-2021-45105-detection", false, `Disable detection of CVE-2021-45105 in versions up to 2.16.0`)
cmd.Flags().BoolVar(&disableCVE44832, "disable-cve-2021-44832-detection", false, `Disable detection of CVE-2021-44832 in versions up to 2.17.0`)
cmd.Flags().BoolVar(&disableUnknownVersions, "disable-unknown-versions", false, `Only output issues if the version of log4j can be determined (note that this will cause certain detection mechanisms to be skipped)`)
cmd.Flags().BoolVar(&outputJSON, "json", false, "If true, output will be in JSON format")
cmd.Flags().BoolVar(&outputFilePathOnly, "file-path-only", false, "If true, output will consist of only paths to the files in which CVEs are detected")
Expand Down
3 changes: 3 additions & 0 deletions cmd/delete.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import (
func deleteCmd() *cobra.Command {
var (
cmdCrawlFlags crawlFlags
cmdCVEFlags cveFlags
dryRun bool
skipOwnerCheck bool
filepathOwners []string
Expand Down Expand Up @@ -82,12 +83,14 @@ When used on windows, deleting based on file ownership is unsupported and skip-o
Logger: logger(cmd, cmdCrawlFlags),
FilepathMatch: filepathMatch,
FindingMatch: findingMatch,
VersionsMatch: deleter.VersionMatcher(cmdCVEFlags.cveResolver()),
DryRun: dryRun,
}.Process, cmd.OutOrStdout(), cmd.OutOrStderr())
return err
},
}
applyCrawlFlags(&cmd, &cmdCrawlFlags)
applyCVEFlags(&cmd, &cmdCVEFlags)
cmd.Flags().BoolVar(&dryRun, "dry-run", true, "When true, a line with be output instead of deleting a file. Use --dry-run=false to enable deletion.")
cmd.Flags().BoolVar(&skipOwnerCheck, "skip-owner-check", false, "When provided, the owner of a file will not be checked before attempting a delete.")
cmd.Flags().StringSliceVar(&filepathOwners, "filepath-owner", nil, `Provide a filepath pattern and owner template that will be used to check whether a file should be deleted or not when it is deemed to be vulnerable.
Expand Down
70 changes: 53 additions & 17 deletions cmd/delete_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -139,13 +139,7 @@ func TestDeleteCmd(t *testing.T) {
var out bytes.Buffer
cmd.SetOut(&out)
require.NoError(t, cmd.Execute())
var found []string
require.NoError(t, filepath.Walk(dir, func(path string, info fs.FileInfo, err error) error {
if !info.IsDir() {
found = append(found, path)
}
return nil
}))
found := readFiles(t, dir)
assert.Equal(t, []string{filepath.Join(dir, "renamed_jar_class_file_extensions/not-a-finding.jar")}, found,
"bad versions should not still be found")
})
Expand All @@ -162,19 +156,61 @@ func TestDeleteCmd(t *testing.T) {
var out bytes.Buffer
cmd.SetOut(&out)
require.NoError(t, cmd.Execute())
var found []string
require.NoError(t, filepath.Walk(dir, func(path string, info fs.FileInfo, err error) error {
if err != nil {
return err
}
if !info.IsDir() {
found = append(found, path)
}
return nil
}))
found := readFiles(t, dir)
assert.Equal(t, []string{filepath.Join(dir, "renamed_jar_class_file_extensions/not-a-finding.jar")}, found,
"bad versions should not still be found")
})

t.Run("Disables CVE-2021-44832 with --disable-cve-2021-44832-detection flag", func(t *testing.T) {
examples := []string{
"cve-2021-44832-versions/log4j-core-2.17.0.jar",
// 2.12.2 should still be deleted as it is vulnerable to CVEs other than cve-2021-44832
"cve-2021-45105-versions/log4j-core-2.12.2.jar",
}
dir := setupExamplesDir(t, examples...)
cmd := deleteCmd()
cmd.SetArgs([]string{dir, "--dry-run=false", "--skip-owner-check", "--disable-cve-2021-44832-detection"})
var out bytes.Buffer
cmd.SetOut(&out)
require.NoError(t, cmd.Execute())
found := readFiles(t, dir)
assert.Equal(t, []string{
filepath.Join(dir, "cve-2021-44832-versions/log4j-core-2.17.0.jar"),
}, found, "bad versions should not still be found")
})

t.Run("Disables CVE-2021-45105 with --disable-cve-2021-45105-detection flag", func(t *testing.T) {
examples := []string{
"cve-2021-44832-versions/log4j-core-2.17.0.jar",
"cve-2021-45105-versions/log4j-core-2.12.2.jar",
}
dir := setupExamplesDir(t, examples...)
cmd := deleteCmd()
// requires both CVE flags so that 2.12.2 does not match on 44832 and still get deleted
cmd.SetArgs([]string{dir, "--dry-run=false", "--skip-owner-check", "--disable-cve-2021-45105-detection", "--disable-cve-2021-44832-detection"})
var out bytes.Buffer
cmd.SetOut(&out)
require.NoError(t, cmd.Execute())
found := readFiles(t, dir)
assert.Equal(t, []string{
filepath.Join(dir, "cve-2021-44832-versions/log4j-core-2.17.0.jar"),
filepath.Join(dir, "cve-2021-45105-versions/log4j-core-2.12.2.jar"),
}, found, "bad versions should not still be found")
})
}

func readFiles(t *testing.T, dir string) []string {
var found []string
require.NoError(t, filepath.Walk(dir, func(path string, info fs.FileInfo, err error) error {
if err != nil {
return err
}
if !info.IsDir() {
found = append(found, path)
}
return nil
}))
return found
}

func setupExamplesDir(t *testing.T, names ...string) string {
Expand Down
22 changes: 22 additions & 0 deletions cmd/flags.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import (

"github.com/palantir/log4j-sniffer/internal/crawler"
"github.com/palantir/log4j-sniffer/pkg/archive"
"github.com/palantir/log4j-sniffer/pkg/crawl"
"github.com/pkg/errors"
"github.com/spf13/cobra"
)
Expand Down Expand Up @@ -125,3 +126,24 @@ func (fs crawlFlags) resolveIgnoreDirs() ([]*regexp.Regexp, error) {
}
return ignores, nil
}

type cveFlags struct {
disableCVE45105 bool
disableCVE44832 bool
}

func (fs cveFlags) cveResolver() crawl.CVEResolver {
var cveResolver crawl.CVEResolver
if fs.disableCVE44832 {
cveResolver.IgnoreCVES = append(cveResolver.IgnoreCVES, crawl.CVE202144832)
}
if fs.disableCVE45105 {
cveResolver.IgnoreCVES = append(cveResolver.IgnoreCVES, crawl.CVE202145105)
}
return cveResolver
}

func applyCVEFlags(cmd *cobra.Command, flags *cveFlags) {
cmd.Flags().BoolVar(&flags.disableCVE45105, "disable-cve-2021-45105-detection", false, `Disable detection of CVE-2021-45105 in versions up to 2.16.0`)
cmd.Flags().BoolVar(&flags.disableCVE44832, "disable-cve-2021-44832-detection", false, `Disable detection of CVE-2021-44832 in versions up to 2.17.0`)
}
6 changes: 5 additions & 1 deletion internal/deleter/delete.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ type Deleter struct {
Logger log.Logger
FilepathMatch func(filepath string) (bool, error)
FindingMatch func(finding crawl.Finding) bool
VersionsMatch func(versions crawl.Versions) bool
DryRun bool
}

Expand All @@ -39,7 +40,7 @@ type Deleter struct {
// nil FilepathMatch and nil FindingMatch functions will act as match alls, asif returning true for all inputs.
// When Deleter.DryRun is true then a line will be logged stating that the file would be deleted.
// When Deleter.Delete is false, then the configured function Delete will be called to delete the file.
func (d Deleter) Process(ctx context.Context, path crawl.Path, finding crawl.Finding, _ crawl.Versions) bool {
func (d Deleter) Process(ctx context.Context, path crawl.Path, finding crawl.Finding, versions crawl.Versions) bool {
if len(path) == 0 {
return true
}
Expand All @@ -57,6 +58,9 @@ func (d Deleter) Process(ctx context.Context, path crawl.Path, finding crawl.Fin
if d.FindingMatch != nil && !d.FindingMatch(finding) {
return true
}
if d.VersionsMatch != nil && !d.VersionsMatch(versions) {
return true
}
if d.DryRun {
d.Logger.Info("Dry-run: would delete %s", filepath)
return false
Expand Down
24 changes: 19 additions & 5 deletions internal/deleter/delete_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,27 +52,40 @@ func TestDeleter_Delete(t *testing.T) {

t.Run("passes first path segment to FilepathMatch", func(t *testing.T) {
var path string
deleter.Deleter{
assert.True(t, deleter.Deleter{
FilepathMatch: func(p string) (bool, error) {
path = p
return false, nil
},
}.Process(context.Background(), crawl.Path{"foo", "bar"}, 0, nil)
}.Process(context.Background(), crawl.Path{"foo", "bar"}, 0, nil))
assert.Equal(t, "foo", path)
})

t.Run("passes finding to finding matcher if path matches", func(t *testing.T) {
var finding crawl.Finding
deleter.Deleter{
assert.True(t, deleter.Deleter{
FilepathMatch: func(string) (bool, error) { return true, nil },
FindingMatch: func(f crawl.Finding) bool {
finding = f
return false
},
}.Process(context.Background(), crawl.Path{"foo", "bar"}, 600, nil)
}.Process(context.Background(), crawl.Path{"foo", "bar"}, 600, nil))
assert.Equal(t, crawl.Finding(600), finding)
})

t.Run("passes versions to version matcher if path and finding matches", func(t *testing.T) {
var versions crawl.Versions
assert.True(t, deleter.Deleter{
FilepathMatch: func(string) (bool, error) { return true, nil },
FindingMatch: func(f crawl.Finding) bool { return true },
VersionsMatch: func(vs crawl.Versions) bool {
versions = vs
return false
},
}.Process(context.Background(), crawl.Path{"foo", "bar"}, 600, crawl.Versions{"foo": {}}))
assert.Equal(t, crawl.Versions{"foo": {}}, versions)
})

t.Run("logs dry mode if running in dry mode", func(t *testing.T) {
file := mustTempFile(t)
var out bytes.Buffer
Expand All @@ -89,7 +102,7 @@ func TestDeleter_Delete(t *testing.T) {
assert.NoError(t, err)
})

t.Run("deletes file if filepath and finding match", func(t *testing.T) {
t.Run("deletes file if filepath, finding and version match", func(t *testing.T) {
file := mustTempFile(t)
var out bytes.Buffer
deleter.Deleter{
Expand All @@ -98,6 +111,7 @@ func TestDeleter_Delete(t *testing.T) {
},
FilepathMatch: func(string) (bool, error) { return true, nil },
FindingMatch: func(crawl.Finding) bool { return true },
VersionsMatch: func(crawl.Versions) bool { return true },
}.Process(context.Background(), crawl.Path{file, "bar"}, 600, nil)
assert.Equal(t, fmt.Sprintf("[INFO] Deleted file %s\n", file), out.String())
assertDoesntExist(t, file)
Expand Down
38 changes: 38 additions & 0 deletions internal/deleter/version.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
// Copyright (c) 2022 Palantir Technologies. All rights reserved.
//
// 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 deleter

import (
"github.com/palantir/log4j-sniffer/pkg/crawl"
)

// CVEResolver resolves Log4jVersions to CVEs
type CVEResolver interface {
CVEs(vs []crawl.Log4jVersion) []string
}

// VersionMatcher creates a function that will return true only when CVEs are present for a
// set of Versions.
// All Versions will be parsed to extract the log4j version found for them, the valid found
// versions will be resolved to matching CVEs using the CVEResolver.
// If any CVEs are present, the returned bool will be true to represent that the file containing
// the CVEs should be deleted.
func VersionMatcher(cveResolver CVEResolver) func(versions crawl.Versions) bool {
return func(versions crawl.Versions) bool {
// ignore fact that there may have been invalid versions
vs, _ := crawl.ParseLog4jVersions(versions)
return cveResolver != nil && len(cveResolver.CVEs(vs)) > 0
}
}
65 changes: 65 additions & 0 deletions internal/deleter/version_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
// Copyright (c) 2022 Palantir Technologies. All rights reserved.
//
// 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 deleter_test

import (
"testing"

"github.com/palantir/log4j-sniffer/internal/deleter"
"github.com/palantir/log4j-sniffer/pkg/crawl"
"github.com/stretchr/testify/assert"
)

func TestVersionMatcher(t *testing.T) {
t.Run("no valid versions do not match", func(t *testing.T) {
assert.False(t, deleter.VersionMatcher(nil)(nil))
})

t.Run("nil version resolver returns false", func(t *testing.T) {
match := deleter.VersionMatcher(nil)
assert.False(t, match(crawl.Versions{"1.2.3": {}}))
})

t.Run("versions passed to resolver", func(t *testing.T) {
var versions []crawl.Log4jVersion
match := deleter.VersionMatcher(stubbedCVEResolver(func(vs []crawl.Log4jVersion) []string {
versions = vs
return nil
}))
assert.False(t, match(crawl.Versions{"1.2.3": {}}))
assert.Equal(t, []crawl.Log4jVersion{{
Original: "1.2.3",
Major: 1,
Minor: 2,
Patch: 3,
}}, versions)
})

t.Run("no cves returns false", func(t *testing.T) {
match := deleter.VersionMatcher(stubbedCVEResolver(func(vs []crawl.Log4jVersion) []string { return nil }))
assert.False(t, match(crawl.Versions{"1.2.3": {}}))
})

t.Run("some cves returns true", func(t *testing.T) {
match := deleter.VersionMatcher(stubbedCVEResolver(func(vs []crawl.Log4jVersion) []string { return []string{"foo"} }))
assert.True(t, match(crawl.Versions{"1.2.3": {}}))
})
}

type stubbedCVEResolver func([]crawl.Log4jVersion) []string

func (s stubbedCVEResolver) CVEs(vs []crawl.Log4jVersion) []string {
return s(vs)
}
Loading