github.com/google/osv-scalibr@v0.4.1/guidedremediation/internal/manifest/python/requirements.go (about) 1 // Copyright 2025 Google LLC 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package python 16 17 import ( 18 "context" 19 "errors" 20 "fmt" 21 "io" 22 "path/filepath" 23 "strings" 24 25 "deps.dev/util/pypi" 26 "deps.dev/util/resolve" 27 "github.com/google/osv-scalibr/extractor/filesystem" 28 "github.com/google/osv-scalibr/extractor/filesystem/language/python/requirements" 29 scalibrfs "github.com/google/osv-scalibr/fs" 30 "github.com/google/osv-scalibr/guidedremediation/internal/manifest" 31 "github.com/google/osv-scalibr/guidedremediation/result" 32 "github.com/google/osv-scalibr/guidedremediation/strategy" 33 "github.com/google/osv-scalibr/log" 34 ) 35 36 type requirementsReadWriter struct{} 37 38 // GetRequirementsReadWriter returns a ReadWriter for requirements.txt manifest files. 39 func GetRequirementsReadWriter() (manifest.ReadWriter, error) { 40 return requirementsReadWriter{}, nil 41 } 42 43 // System returns the ecosystem of this ReadWriter. 44 func (r requirementsReadWriter) System() resolve.System { 45 return resolve.PyPI 46 } 47 48 // SupportedStrategies returns the remediation strategies supported for this manifest. 49 func (r requirementsReadWriter) SupportedStrategies() []strategy.Strategy { 50 return []strategy.Strategy{strategy.StrategyRelax} 51 } 52 53 // Read parses the manifest from the given file. 54 func (r requirementsReadWriter) Read(path string, fsys scalibrfs.FS) (manifest.Manifest, error) { 55 path = filepath.ToSlash(path) 56 f, err := fsys.Open(path) 57 if err != nil { 58 return nil, err 59 } 60 defer f.Close() 61 62 inv, err := requirements.NewDefault().Extract(context.Background(), &filesystem.ScanInput{ 63 FS: fsys, 64 Path: path, 65 Root: filepath.Dir(path), 66 Reader: f, 67 }) 68 if err != nil { 69 return nil, err 70 } 71 72 var reqs []resolve.RequirementVersion 73 for _, pkg := range inv.Packages { 74 m := pkg.Metadata.(*requirements.Metadata) 75 if len(m.HashCheckingModeValues) > 0 { 76 return nil, errors.New("requirements file in hash checking mode not supported as manifest") 77 } 78 d, err := pypi.ParseDependency(m.Requirement) 79 if err != nil { 80 return nil, err 81 } 82 reqs = append(reqs, resolve.RequirementVersion{ 83 VersionKey: resolve.VersionKey{ 84 PackageKey: resolve.PackageKey{ 85 System: resolve.PyPI, 86 Name: pkg.Name, 87 }, 88 Version: d.Constraint, 89 VersionType: resolve.Requirement, 90 }, 91 }) 92 } 93 94 return &pythonManifest{ 95 filePath: path, 96 root: resolve.Version{ 97 VersionKey: resolve.VersionKey{ 98 PackageKey: resolve.PackageKey{ 99 System: resolve.PyPI, 100 Name: "rootproject", 101 }, 102 VersionType: resolve.Concrete, 103 Version: "1.0.0", 104 }, 105 }, 106 requirements: reqs, 107 groups: make(map[manifest.RequirementKey][]string), 108 }, nil 109 } 110 111 // Write writes the manifest after applying the patches to outputPath. 112 func (r requirementsReadWriter) Write(original manifest.Manifest, fsys scalibrfs.FS, patches []result.Patch, outputPath string) error { 113 return write(fsys, original.FilePath(), outputPath, patches, updateRequirements) 114 } 115 116 // updateRequirements takes an io.Reader representing the requirements.txt file 117 // and a map of package names to their new version constraints, returns the 118 // file with the updated requirements as a string. 119 func updateRequirements(reader io.Reader, requirements []TokenizedRequirements) (string, error) { 120 data, err := io.ReadAll(reader) 121 if err != nil { 122 return "", fmt.Errorf("error reading requirements: %w", err) 123 } 124 125 var sb strings.Builder 126 for _, line := range strings.SplitAfter(string(data), "\n") { 127 if strings.TrimSpace(line) == "" { 128 sb.WriteString(line) 129 continue 130 } 131 132 reqLine := line 133 // We should trim the comments so they are not part of the requirement. 134 if i := strings.Index(reqLine, "#"); i != -1 { 135 reqLine = reqLine[:i] 136 } 137 if strings.TrimSpace(reqLine) == "" { 138 sb.WriteString(line) 139 continue 140 } 141 142 d, err := pypi.ParseDependency(reqLine) 143 if err != nil { 144 log.Warnf("failed to parse Python dependency %s: %v", line, err) 145 sb.WriteString(line) 146 continue 147 } 148 149 newReq, ok := findTokenizedRequirement(requirements, d.Name, tokenizeRequirement(d.Constraint)) 150 if !ok { 151 // We don't need to update the requirement of this dependency. 152 sb.WriteString(line) 153 continue 154 } 155 sb.WriteString(replaceRequirement(line, newReq)) 156 } 157 158 return sb.String(), nil 159 }