Skip to content
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
2 changes: 1 addition & 1 deletion .github/workflows/cxone-scan.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ jobs:
- name: Checkout
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
- name: Checkmarx One Scan
uses: checkmarx/ast-github-action@f0869bd1a37fddc06499a096101e6c900e815d81 # v.2.0.36
uses: checkmarx/ast-github-action@327efb5d1dd16ac6c7c21a9ff8ec1e8ec393b5e6 # v.2.3.46
with:
base_uri: ${{ secrets.AST_RND_SCANS_BASE_URI }}
cx_tenant: ${{ secrets.AST_RND_SCANS_TENANT }}
Expand Down
71 changes: 56 additions & 15 deletions internal/parsers/npm/package_json_parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"os"
"path/filepath"
"sort"
"strconv"
"strings"

"github.com/Checkmarx/manifest-parser/pkg/parser/models"
Expand Down Expand Up @@ -191,18 +192,14 @@ func (p *NpmPackageJsonParser) Parse(manifestFile string) ([]models.Package, err
// - Falls back to sensible defaults if necessary
func getResolvedVersion(name, specVersion string, lock lockFile) string {
// Check if version is already exact - if so, return it directly
if !strings.HasPrefix(specVersion, "^") &&
!strings.HasPrefix(specVersion, "~") &&
!strings.Contains(specVersion, "*") &&
!strings.Contains(specVersion, ">") &&
!strings.Contains(specVersion, "<") &&
!strings.Contains(specVersion, "latest") {

if !checkRangeSpecifiersPresent(specVersion) && !strings.Contains(specVersion, "latest") {
return specVersion
}

// Try v1 format first
if deps := lock.Dependencies; deps != nil {
if entry, ok := deps[name]; ok && entry.Version != "" {
if entry, ok := deps[name]; ok && entry.Version != "" && isLockVersionGreater(specVersion, entry.Version) {
return entry.Version
}
}
Expand All @@ -215,25 +212,69 @@ func getResolvedVersion(name, specVersion string, lock lockFile) string {
"node_modules/" + name + "@" + specVersion,
"node_modules/" + name + "@" + strings.TrimPrefix(specVersion, "^"),
"node_modules/" + name + "@" + strings.TrimPrefix(specVersion, "~"),
"", // Root package
}

for _, path := range pathVariations {
if entry, ok := pkgs[path]; ok && entry.Version != "" {
if entry, ok := pkgs[path]; ok && entry.Version != "" && isLockVersionGreater(specVersion, entry.Version) {
return entry.Version
}
}
}

// For version specifiers, return "latest" as fallback
if strings.HasPrefix(specVersion, "^") ||
strings.HasPrefix(specVersion, "~") ||
strings.Contains(specVersion, "*") ||
strings.Contains(specVersion, ">") ||
strings.Contains(specVersion, "<") {

if checkRangeSpecifiersPresent(specVersion) {
return "latest"
}

// Otherwise return the specified version
return specVersion
}
func isLockVersionGreater(specVersion, lockVersion string) bool {
specVersion = stripRangeSpecifier(specVersion)
lockVersion = stripRangeSpecifier(lockVersion)

specParts := strings.Split(specVersion, ".")
lockParts := strings.Split(lockVersion, ".")

maxLen := len(specParts)
if len(lockParts) > maxLen {
maxLen = len(lockParts)
}
for i := 0; i < maxLen; i++ {
var specPart, lockPart int
if i < len(specParts) {
specPart, _ = strconv.Atoi(specParts[i])
}
if i < len(lockParts) {
lockPart, _ = strconv.Atoi(lockParts[i])
}
if lockPart == specPart {
continue
}
if lockPart > specPart {
return true
} else if lockPart < specPart {
return false
}
}
// In case if the lock version is same as the spec version, we consider it,
// as range specifiers indicate version greaterThan or equalTo
return true
}

func stripRangeSpecifier(version string) string {
if checkRangeSpecifiersPresent(version) {
return version[1:]
}
return version
}
func checkRangeSpecifiersPresent(version string) bool {
if strings.HasPrefix(version, "^") ||
strings.HasPrefix(version, "~") ||
strings.Contains(version, "*") ||
strings.Contains(version, ">") ||
strings.Contains(version, "<") {
return true
}
return false
}
43 changes: 43 additions & 0 deletions internal/parsers/npm/package_json_parser_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -701,3 +701,46 @@ func TestSectionAwareParsing(t *testing.T) {
t.Errorf("expected %d packages, got %d", expectedCount, len(packages))
}
}

// TestGetResolvedVersionComparisonWithLock tests comparison logic between package.json spec and package-lock.json
func TestGetResolvedVersionComparisonWithLock(t *testing.T) {
// Create a lock file JSON with various scenarios:
// - foo: same version as spec (should return lock version)
// - bar: lock version smaller than spec (should return "latest")
// - baz: lock version greater than spec (should return lock version)
// - missing: not present in lock (should return "latest")
lockJSON := `{
"lockfileVersion": 2,
"packages": {
"node_modules/foo": { "version": "1.2.3" },
"node_modules/bar": { "version": "1.2.2" },
"node_modules/baz": { "version": "2.0.0" }
}
}`

var lock lockFile
if err := json.Unmarshal([]byte(lockJSON), &lock); err != nil {
t.Fatalf("failed to unmarshal lock JSON: %v", err)
}

tests := []struct {
name string
pkg string
spec string
expected string
}{
{"lock equal to spec", "foo", "^1.2.3", "1.2.3"},
{"lock smaller than spec", "bar", "^1.2.3", "latest"},
{"lock greater than spec", "baz", "^1.2.0", "2.0.0"},
{"lock missing", "missing", "^3.0.0", "latest"},
}

for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
res := getResolvedVersion(tc.pkg, tc.spec, lock)
if res != tc.expected {
t.Fatalf("expected %q for %s, got %q", tc.expected, tc.pkg, res)
}
})
}
}
Loading