Skip to content

Commit

Permalink
Swap JetBrains EAP versions for maxed last major release for vuln che…
Browse files Browse the repository at this point in the history
…ck purposes

For #22723
  • Loading branch information
iansltx committed Dec 14, 2024
1 parent fa385c7 commit b5faf81
Show file tree
Hide file tree
Showing 2 changed files with 101 additions and 0 deletions.
57 changes: 57 additions & 0 deletions server/vulnerabilities/nvd/cpe.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"path/filepath"
"regexp"
"slices"
"strconv"
"strings"
"time"
"unicode"
Expand Down Expand Up @@ -182,6 +183,60 @@ func cpeGeneralSearchQuery(software *fleet.Software) (string, []interface{}, err
return stm, args, nil
}

// softwareTransformers provide logic for tweaking e.g. software versions to match what's in the NVD database. These
// changes are done here rather than in sanitizeSoftware to ensure that software versions visible in the UI are the
// raw version strings.
var (
softwareTransformers = []struct {
matches func(*fleet.Software) bool
mutate func(*fleet.Software, log.Logger)
}{
{
// JetBrains EAP version numbers aren't what are used in CPEs; this handles the translation for Mac versions.
// See #22723 for background. Bundle identifier for EAPs also ends with "-EAP" but checking version makes it
// a bit easier to add other platforms later. EAP version numbers are e.g. EAP GO-243.21565.42, and checking
// here for the dash ensures that string splitting in the mutator always works without a bounds check.
matches: func(s *fleet.Software) bool {
return s.BundleIdentifier != "" && strings.HasPrefix(s.BundleIdentifier, "com.jetbrains.") &&
strings.HasPrefix(s.Version, "EAP ") && strings.Contains(s.Version, "-")
},
mutate: func(s *fleet.Software, logger log.Logger) {
// 243 -> 2024.3
eapMajorVersion := strings.Split(strings.Split(s.Version, "-")[1], ".")[0]
yearBasedMajorVersion, err := strconv.Atoi("20" + eapMajorVersion[:2])
if err != nil {
level.Debug(logger).Log("msg", "failed to parse JetBrains EAP major version", "version", s.Version, "err", err)
return
}
yearBasedMinorVersion, err := strconv.Atoi(eapMajorVersion[2:])
if err != nil {
level.Debug(logger).Log("msg", "failed to parse JetBrains EAP minor version", "version", s.Version, "err", err)
return
}

// EAPs are treated as having all fixes from the previous year-based release, but no fixes from the
// year-based release they're an EAP of.
yearBasedMinorVersion -= 1
if yearBasedMinorVersion <= 0 { // wrap e.g. 2024.1 to 2023.4
yearBasedMajorVersion -= 1
yearBasedMinorVersion = 4
}

s.Version = fmt.Sprintf("%d.%d.%s", yearBasedMajorVersion, yearBasedMinorVersion, "999")
},
},
}
)

func mutateSoftware(software *fleet.Software, logger log.Logger) {
for _, transformer := range softwareTransformers {
if transformer.matches(software) {
transformer.mutate(software, logger)
break
}
}
}

// CPEFromSoftware attempts to find a matching cpe entry for the given software in the NVD CPE dictionary. `db` contains data from the NVD CPE dictionary
// and is optimized for lookups, see `GenerateCPEDB`. `translations` are used to aid in cpe matching. When searching for cpes, we first check if it matches
// any translations, and then lookup in the cpe database based on the title, product and vendor.
Expand All @@ -191,6 +246,8 @@ func CPEFromSoftware(logger log.Logger, db *sqlx.DB, software *fleet.Software, t
return "", nil
}

mutateSoftware(software, logger) // tweak e.g. software versions prior to CPE matching if needed

translation, match, err := translations.Translate(reCache, software)
if err != nil {
return "", fmt.Errorf("translate software: %w", err)
Expand Down
44 changes: 44 additions & 0 deletions server/vulnerabilities/nvd/cpe_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,40 @@ import (
"github.com/stretchr/testify/require"
)

func TestCPESoftwareMutations(t *testing.T) {
logger := log.NewNopLogger()

// For JetBrains EAPs; see #22723
normalJetBrainsTitle := &fleet.Software{
Name: "GoLand.app",
Version: "2024.3.1",
BundleIdentifier: "com.jetbrains.goland",
}
mutateSoftware(normalJetBrainsTitle, logger)
require.Equal(t, normalJetBrainsTitle.Version, "2024.3.1")
normalJetBrainsTitle.Version = "2024.3"
mutateSoftware(normalJetBrainsTitle, logger)
require.Equal(t, normalJetBrainsTitle.Version, "2024.3")

lateYearEAP := &fleet.Software{
Name: "GoLand.app",
Version: "EAP GO-243.21565.42",
BundleIdentifier: "com.jetbrains.goland-EAP",
}
mutateSoftware(lateYearEAP, logger)
require.Equal(t, lateYearEAP.Version, "2024.2.999")
mutateSoftware(lateYearEAP, logger) // at this point we shouldn't mutate anything further
require.Equal(t, lateYearEAP.Version, "2024.2.999")

earlyYearEAP := &fleet.Software{
Name: "IntelliJ IDEA CE",
Version: "EAP IC-241.12345.67",
BundleIdentifier: "com.jetbrains.intellij",
}
mutateSoftware(earlyYearEAP, logger)
require.Equal(t, earlyYearEAP.Version, "2023.4.999")
}

func TestCPEFromSoftware(t *testing.T) {
tempDir := t.TempDir()

Expand Down Expand Up @@ -1364,6 +1398,16 @@ func TestCPEFromSoftwareIntegration(t *testing.T) {
},
cpe: "cpe:2.3:a:jetbrains:pycharm:2022.1:*:*:*:*:macos:*:*",
},
{ // Revise EAP JebBrains products to match "end of previous release"
software: fleet.Software{
Name: "IntelliJ IDEA 2024.3 EAP.app",
Source: "apps",
Version: "EAP IU-242.16677.21",
Vendor: "",
BundleIdentifier: "com.jetbrains.intellij-EAP",
},
cpe: "cpe:2.3:a:jetbrains:intellij_idea:2024.1.999:*:*:*:*:macos:*:*",
},
{
software: fleet.Software{
Name: "eamodio.gitlens",
Expand Down

0 comments on commit b5faf81

Please sign in to comment.