Skip to content

Commit b7e39e2

Browse files
committed
polish quickfix algorithm
1 parent 12451c9 commit b7e39e2

1 file changed

Lines changed: 132 additions & 138 deletions

File tree

cmd/devguard-cli/test/quickfix.go

Lines changed: 132 additions & 138 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,6 @@ func getVersion(packageManager string, pkg RegistryRequest) (*http.Response, err
4343
func getRecommendedVersions(npmResponse NPMResponse, currentVersion string) ([]string, error) {
4444
var versions [][]string
4545

46-
// Extract and filter versions from NPMResponse
4746
for _, obj := range npmResponse.Versions {
4847
// skip release candidates
4948
if strings.Contains(obj.Version, "-") {
@@ -53,15 +52,18 @@ func getRecommendedVersions(npmResponse NPMResponse, currentVersion string) ([]s
5352
versions = append(versions, versionParts)
5453
}
5554

56-
// Filter by major version and sort
5755
var currentMajor, currentMinor, currentPatch int
58-
fmt.Sscanf(currentVersion, "%d.%d.%d", &currentMajor, &currentMinor, &currentPatch)
56+
if _, err := fmt.Sscanf(currentVersion, "%d.%d.%d", &currentMajor, &currentMinor, &currentPatch); err != nil {
57+
return nil, fmt.Errorf("invalid current version format: %s", currentVersion)
58+
}
5959

6060
var recommended []string
6161
for _, version := range versions {
6262
var major, minor, patch int
6363
versionStr := strings.Join(version, ".")
64-
fmt.Sscanf(versionStr, "%d.%d.%d", &major, &minor, &patch)
64+
if _, err := fmt.Sscanf(versionStr, "%d.%d.%d", &major, &minor, &patch); err != nil {
65+
continue
66+
}
6567

6668
if major == currentMajor {
6769
if minor >= currentMinor {
@@ -90,7 +92,9 @@ func getRecommendedVersions(npmResponse NPMResponse, currentVersion string) ([]s
9092

9193
func parseVersion(version string) [3]int {
9294
var result [3]int
93-
fmt.Sscanf(version, "%d.%d.%d", &result[0], &result[1], &result[2])
95+
if _, err := fmt.Sscanf(version, "%d.%d.%d", &result[0], &result[1], &result[2]); err != nil {
96+
return [3]int{0, 0, 0}
97+
}
9498
return result
9599
}
96100

@@ -108,87 +112,41 @@ func IsValidSemver(version string) bool {
108112
return matched
109113
}
110114

111-
func processDependencies(depMap map[string]string, depName string, depVersion string, visited map[string]bool, vulnerablePackage string, vulnerableVersion string, node *DependencyNode) {
112-
for depKey, depVal := range depMap {
113-
// remove ^, ~, quotes
114-
normalizedDepVal := strings.Trim(depVal, "^~\"")
115-
116-
// Skip non-semver versions
117-
if !IsValidSemver(normalizedDepVal) {
118-
continue
119-
}
120-
121-
// Check if version exists before fetching
122-
if !VersionExists(depKey, normalizedDepVal) {
123-
fmt.Printf("Skipping %s@%s: version not found\n", depKey, normalizedDepVal)
124-
continue
125-
}
126-
127-
depResp, err := GetNPMRegistry(RegistryRequest{Dependency: depKey, Version: depVal})
128-
if err != nil {
129-
fmt.Printf("Error fetching %s@%s: %v\n", depKey, depVal, err)
130-
continue
131-
}
132-
133-
depBody, err := io.ReadAll(depResp.Body)
134-
depResp.Body.Close()
135-
if err != nil {
136-
fmt.Printf("Error reading response for %s@%s: %v\n", depKey, depVal, err)
137-
continue
138-
}
139-
140-
// Recursive call
141-
childNode := walkDependencyTree(depBody, depKey, depVal, visited, vulnerablePackage, vulnerableVersion)
142-
if childNode != nil {
143-
node.Dependencies[depKey] = childNode
144-
}
145-
if depKey == vulnerablePackage && depVal == vulnerableVersion {
146-
fmt.Printf("Vulnerable package found: %s@%s\n", depKey, depVal)
147-
}
115+
func parsePurl(purl string) (string, string, error) {
116+
// Format: pkg:npm/package-name@version
117+
if !strings.HasPrefix(purl, "pkg:npm/") {
118+
return "", "", fmt.Errorf("invalid purl format: %s", purl)
148119
}
149-
}
150-
151-
func walkDependencyTree(npmRegisterResp []byte, depName string, depVersion string, visited map[string]bool, vulnerablePackage string, vulnerableVersion string) *DependencyNode {
152-
var jsonData NPMResponse
153120

154-
if err := json.Unmarshal(npmRegisterResp, &jsonData); err != nil {
155-
return nil
121+
purl = strings.TrimPrefix(purl, "pkg:npm/")
122+
parts := strings.Split(purl, "@")
123+
if len(parts) != 2 {
124+
return "", "", fmt.Errorf("invalid purl format: %s", purl)
156125
}
157126

158-
nodeKey := depName + "@" + depVersion
159-
if visited[nodeKey] {
160-
return nil
161-
}
162-
visited[nodeKey] = true
163-
164-
node := &DependencyNode{
165-
Name: depName,
166-
Version: depVersion,
167-
Dependencies: make(map[string]*DependencyNode),
168-
}
169-
170-
if jsonData.Dependencies == nil && jsonData.DevDependencies == nil && jsonData.PeerDependencies == nil && jsonData.OptionalDependencies == nil {
171-
return node
172-
}
173-
174-
// Process all dependency types using the same logic
175-
processDependencies(jsonData.Dependencies, depName, depVersion, visited, vulnerablePackage, vulnerableVersion, node)
176-
processDependencies(jsonData.OptionalDependencies, depName, depVersion, visited, vulnerablePackage, vulnerableVersion, node)
177-
processDependencies(jsonData.DevDependencies, depName, depVersion, visited, vulnerablePackage, vulnerableVersion, node)
127+
return parts[0], parts[1], nil
128+
}
178129

179-
return node
130+
func normalizeVersion(version string) string {
131+
return strings.Trim(version, "^~\"")
180132
}
181133

182-
func printDependencyTree(node *DependencyNode, indent string) {
183-
if node == nil {
184-
return
134+
func getAllDependencyMaps(depMeta *NPMResponse) []map[string]string {
135+
return []map[string]string{
136+
depMeta.Dependencies,
137+
depMeta.PeerDependencies,
138+
depMeta.OptionalDependencies,
139+
depMeta.DevDependencies,
185140
}
141+
}
186142

187-
fmt.Printf("%s%s@%s\n", indent, node.Name, node.Version)
188-
189-
for _, dep := range node.Dependencies {
190-
printDependencyTree(dep, indent+" ")
143+
func findDependencyVersionInMeta(depMeta *NPMResponse, pkgName string) string {
144+
for _, depType := range getAllDependencyMaps(depMeta) {
145+
if version, ok := depType[pkgName]; ok {
146+
return version
147+
}
191148
}
149+
return ""
192150
}
193151

194152
func findDependencyVersion(npmResp NPMResponse, depName string) string {
@@ -208,97 +166,133 @@ func findDependencyVersion(npmResp NPMResponse, depName string) string {
208166
return ""
209167
}
210168

211-
func fetchPackageMetadata(pkgManager string, dep string, version string) (*NPMResponse, error) {
212-
resp, err := getVersion(pkgManager, RegistryRequest{Dependency: dep, Version: version})
213-
if err != nil {
214-
return nil, fmt.Errorf("error fetching %s@%s: %w", dep, version, err)
215-
}
169+
func checkVulnerabilityFixChain(purls []string, fixedVersion string) (bool, error) {
216170

217-
body, err := io.ReadAll(resp.Body)
218-
resp.Body.Close()
219-
if err != nil {
220-
return nil, fmt.Errorf("error reading response for %s@%s: %w", dep, version, err)
221-
}
171+
packages := make([]struct {
172+
name string
173+
version string
174+
}, len(purls))
222175

223-
var npmResp NPMResponse
224-
if err := json.Unmarshal(body, &npmResp); err != nil {
225-
return nil, fmt.Errorf("error unmarshalling JSON for %s@%s: %w", dep, version, err)
176+
for i, purl := range purls {
177+
name, version, err := parsePurl(purl)
178+
if err != nil {
179+
return false, err
180+
}
181+
packages[i].name = name
182+
packages[i].version = version
226183
}
227184

228-
return &npmResp, nil
229-
}
185+
for i := 0; i < len(packages)-1; i++ {
186+
pkgName := packages[i].name
187+
currentVersion := packages[i].version
230188

231-
func checkVersionAvailability(versions []string, currentVersion string) (string, error) {
232-
if len(versions) == 0 {
233-
return "", fmt.Errorf("no versions available")
234-
}
189+
// fetch all version
190+
allVersionsMeta, err := fetchPackageMetadata(getPackageManager("npm"), pkgName, "")
191+
if err != nil {
192+
return false, fmt.Errorf("failed to fetch all versions for %s: %w", pkgName, err)
193+
}
235194

236-
if versions[0] == currentVersion {
237-
return "", fmt.Errorf("no new version available (current: %s)", currentVersion)
238-
}
195+
// get major versions and sort
196+
versions, err := getRecommendedVersions(*allVersionsMeta, currentVersion)
197+
if err != nil {
198+
return false, fmt.Errorf("failed to get recommended versions for %s: %w", pkgName, err)
199+
}
239200

240-
return versions[0], nil
241-
}
201+
if len(versions) == 0 {
202+
return false, fmt.Errorf("no newer version available for %s@%s in the same major band", pkgName, currentVersion)
203+
}
242204

243-
func checkVulnerabilityStatus(latestMeta *NPMResponse, vulnPkg string, vulnVer string) (bool, string) {
244-
latestVulnVer := findDependencyVersion(*latestMeta, vulnPkg)
205+
latestVersion := versions[0]
206+
if latestVersion == currentVersion {
207+
return false, fmt.Errorf("no new version available for %s (current: %s)", pkgName, currentVersion)
208+
}
245209

246-
if latestVulnVer == vulnVer {
247-
return false, latestVulnVer
248-
}
210+
fmt.Printf("Found newer version for %s: %s to %s\n", pkgName, currentVersion, latestVersion)
249211

250-
if latestVulnVer != "" && latestVulnVer != vulnVer {
251-
return true, latestVulnVer
252-
}
212+
// Second: check latest version
213+
latestMeta, err := fetchPackageMetadata(getPackageManager("npm"), pkgName, latestVersion)
214+
if err != nil {
215+
return false, fmt.Errorf("failed to fetch latest metadata for %s@%s: %w", pkgName, latestVersion, err)
216+
}
253217

254-
return false, latestVulnVer
255-
}
218+
nextPkgName := packages[i+1].name
256219

257-
func checkVulnerabilityFix(directDep string, currentVer string, vulnPkg string, vulnVer string) error {
220+
nextVersionInLatest := findDependencyVersion(*latestMeta, nextPkgName)
221+
if nextVersionInLatest == "" {
222+
return false, fmt.Errorf("package %s not found in %s@%s dependencies", nextPkgName, pkgName, latestVersion)
223+
}
258224

259-
npmMeta, err := fetchPackageMetadata(getPackageManager("npm"), directDep, "")
260-
if err != nil {
261-
return fmt.Errorf("failed to fetch package metadata: %w", err)
225+
normalizedNextVersion := normalizeVersion(nextVersionInLatest)
226+
fmt.Printf(" %s found in %s@%s dependencies: %s\n", nextPkgName, pkgName, latestVersion, normalizedNextVersion)
227+
228+
packages[i+1].version = normalizedNextVersion
262229
}
263230

264-
versions, err := getRecommendedVersions(*npmMeta, currentVer)
265-
if err != nil {
266-
return fmt.Errorf("failed to filter versions: %w", err)
231+
vulnPkgName := packages[len(packages)-1].name
232+
vulnVersion := packages[len(packages)-1].version
233+
234+
if !IsValidSemver(vulnVersion) {
235+
return false, fmt.Errorf("vulnerable package has invalid semver: %s@%s", vulnPkgName, vulnVersion)
267236
}
268237

269-
latestVer, err := checkVersionAvailability(versions, currentVer)
270-
if err != nil {
271-
fmt.Println(err)
272-
return err
238+
// Parse versions to compare
239+
vulnParts := parseVersion(vulnVersion)
240+
fixedParts := parseVersion(fixedVersion)
241+
242+
isFixed := false
243+
if vulnParts[0] > fixedParts[0] {
244+
isFixed = true
245+
} else if vulnParts[0] == fixedParts[0] {
246+
if vulnParts[1] > fixedParts[1] {
247+
isFixed = true
248+
} else if vulnParts[1] == fixedParts[1] {
249+
if vulnParts[2] >= fixedParts[2] {
250+
isFixed = true
251+
}
252+
}
253+
}
254+
255+
if isFixed {
256+
fmt.Printf("Fix verified: %s@%s is >= %s (fixed version)\n", vulnPkgName, vulnVersion, fixedVersion)
257+
return true, nil
273258
}
274259

275-
fmt.Printf("New versions available for %s: %s -> %s\n", directDep, currentVer, latestVer)
260+
fmt.Printf("Fix not verified: %s@%s is < %s\n", vulnPkgName, vulnVersion, fixedVersion)
261+
return false, nil
262+
}
276263

277-
latestMeta, err := fetchPackageMetadata(getPackageManager("npm"), directDep, "latest")
264+
func fetchPackageMetadata(pkgManager string, dep string, version string) (*NPMResponse, error) {
265+
resp, err := getVersion(pkgManager, RegistryRequest{Dependency: dep, Version: version})
278266
if err != nil {
279-
return fmt.Errorf("failed to fetch latest version metadata: %w", err)
267+
return nil, fmt.Errorf("error fetching %s@%s: %w", dep, version, err)
280268
}
281269

282-
isFixed, newVer := checkVulnerabilityStatus(latestMeta, vulnPkg, vulnVer)
270+
body, err := io.ReadAll(resp.Body)
271+
resp.Body.Close()
272+
if err != nil {
273+
return nil, fmt.Errorf("error reading response for %s@%s: %w", dep, version, err)
274+
}
283275

284-
if !isFixed {
285-
fmt.Printf("Vulnerability NOT fixed in latest %s (still uses %s@%s)\n", directDep, vulnPkg, vulnVer)
286-
return nil
276+
var npmResp NPMResponse
277+
if err := json.Unmarshal(body, &npmResp); err != nil {
278+
return nil, fmt.Errorf("error unmarshalling JSON for %s@%s: %w", dep, version, err)
287279
}
288280

289-
fmt.Printf("✓ Vulnerability FIXED in latest (uses %s@%s instead of %s)\n", vulnPkg, newVer, vulnVer)
290-
return nil
281+
return &npmResp, nil
291282
}
292283

293284
func main() {
294-
directDependency := "playwright"
295-
currentVersion := "1.50.1"
296-
directVulnerablePackage := "fsevents"
297-
directVulnerableVersion := "2.3.2"
298-
// transitiveVulnerablePackage := "ip"
299-
// transitiveVulnerableVersion := "1.1.5"
300-
301-
if err := checkVulnerabilityFix(directDependency, currentVersion, directVulnerablePackage, directVulnerableVersion); err != nil {
285+
purls := []string{
286+
"pkg:npm/react-markdown@9.0.1",
287+
"pkg:npm/remark-rehype@11.1.1",
288+
"pkg:npm/mdast-util-to-hast@13.2.0",
289+
}
290+
fixedVersion := "13.2.1"
291+
292+
isFixed, err := checkVulnerabilityFixChain(purls, fixedVersion)
293+
if err != nil {
302294
fmt.Println("Error:", err)
295+
return
303296
}
297+
fmt.Println(isFixed)
304298
}

0 commit comments

Comments
 (0)