Skip to content

Commit 1512dc6

Browse files
authored
Merge pull request #76 from scanoss/feature/mdaloia/SP-1801-Scanoss-Py-Add-replace-action-to-scanoss.json-post-processing
feat: SP-1801 Add support for replace when specifying settings file
2 parents 0937e0c + be95511 commit 1512dc6

18 files changed

+552
-309
lines changed

.github/workflows/python-local-test.yml

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,10 @@ on:
55
workflow_dispatch:
66
push:
77
branches:
8-
- 'main'
8+
- "main"
99
pull_request:
1010
branches:
11-
- 'main'
11+
- "main"
1212

1313
permissions:
1414
contents: read
@@ -22,7 +22,7 @@ jobs:
2222
- name: Set up Python
2323
uses: actions/setup-python@v5
2424
with:
25-
python-version: '3.10.x'
25+
python-version: "3.10.x"
2626

2727
- name: Install Dependencies
2828
run: |
@@ -78,3 +78,8 @@ jobs:
7878
echo "Error: WFP test did not produce any results. Failing"
7979
exit 1
8080
fi
81+
82+
- name: Run Unit Tests
83+
run: |
84+
python -m unittest
85+

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,3 +26,6 @@ local-*.txt
2626
docs/build
2727
.devcontainer/devcontainer.json
2828
!.devcontainer/*.example.json
29+
30+
31+
!tests/data/*.json

CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
99
### Added
1010
- Upcoming changes...
1111

12+
## [1.18.0] - 2024-11-11
13+
### Fixed
14+
- Fixed post processor being accesed if not set
15+
### Added
16+
- Add support for replace action when specifying a settings file
17+
- Add replaced files as context to scan request
18+
1219
## [1.17.5] - 2024-11-12
1320
### Fixed
1421
- Fix dependencies scan result structure

requirements.txt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,5 @@ pypac
88
urllib3
99
pyOpenSSL
1010
google-api-core
11-
importlib_resources
11+
importlib_resources
12+
packageurl-python

setup.cfg

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@ install_requires =
3535
pyOpenSSL
3636
google-api-core
3737
importlib_resources
38+
packageurl-python
39+
3840

3941
[options.extras_require]
4042
fast_winnowing =

src/scanoss/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,4 +22,4 @@
2222
THE SOFTWARE.
2323
"""
2424

25-
__version__ = '1.17.5'
25+
__version__ = "1.18.0"

src/scanoss/scanner.py

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -161,12 +161,13 @@ def __init__(self, wfp: str = None, scan_output: str = None, output_format: str
161161
if skip_extensions: # Append extra file extensions to skip
162162
self.skip_extensions.extend(skip_extensions)
163163

164-
if scan_settings:
165-
self.scan_settings = scan_settings
166-
self.post_processor = ScanPostProcessor(scan_settings, debug=debug, trace=trace, quiet=quiet)
167-
self._maybe_set_api_sbom()
164+
self.scan_settings = scan_settings
165+
self.post_processor = ScanPostProcessor(scan_settings, debug=debug, trace=trace, quiet=quiet) if scan_settings else None
166+
self._maybe_set_api_sbom()
168167

169168
def _maybe_set_api_sbom(self):
169+
if not self.scan_settings:
170+
return
170171
sbom = self.scan_settings.get_sbom()
171172
if sbom:
172173
self.scanoss_api.set_sbom(sbom)
@@ -521,11 +522,12 @@ def __finish_scan_threaded(self, file_map: Optional[Dict[Any, Any]] = None) -> b
521522
success = False
522523
dep_responses = self.threaded_deps.responses
523524

524-
raw_scan_results = self._merge_scan_results(
525-
scan_responses, dep_responses, file_map
526-
)
525+
raw_scan_results = self._merge_scan_results(scan_responses, dep_responses, file_map)
527526

528-
results = self.post_processor.load_results(raw_scan_results).post_process()
527+
if self.post_processor:
528+
results = self.post_processor.load_results(raw_scan_results).post_process()
529+
else:
530+
results = raw_scan_results
529531

530532
if self.output_format == 'plain':
531533
self.__log_result(json.dumps(results, indent=2, sort_keys=True))

src/scanoss/scanoss_settings.py

Lines changed: 56 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -69,15 +69,15 @@ def load_json_file(self, filepath: str):
6969
json_file = Path(filepath).resolve()
7070

7171
if not json_file.exists():
72-
self.print_stderr(f"Scan settings file not found: {filepath}")
72+
self.print_stderr(f'Scan settings file not found: {filepath}')
7373
self.data = {}
7474

75-
with open(json_file, "r") as jsonfile:
76-
self.print_debug(f"Loading scan settings from: {filepath}")
75+
with open(json_file, 'r') as jsonfile:
76+
self.print_debug(f'Loading scan settings from: {filepath}')
7777
try:
7878
self.data = json.load(jsonfile)
7979
except Exception as e:
80-
self.print_stderr(f"ERROR: Problem parsing input JSON: {e}")
80+
self.print_stderr(f'ERROR: Problem parsing input JSON: {e}')
8181
return self
8282

8383
def set_file_type(self, file_type: str):
@@ -91,9 +91,7 @@ def set_file_type(self, file_type: str):
9191
"""
9292
self.settings_file_type = file_type
9393
if not self._is_valid_sbom_file:
94-
raise Exception(
95-
'Invalid scan settings file, missing "components" or "bom")'
96-
)
94+
raise Exception('Invalid scan settings file, missing "components" or "bom")')
9795
return self
9896

9997
def set_scan_type(self, scan_type: str):
@@ -111,7 +109,7 @@ def _is_valid_sbom_file(self):
111109
Returns:
112110
bool: True if the file is valid, False otherwise
113111
"""
114-
if not self.data.get("components") or not self.data.get("bom"):
112+
if not self.data.get('components') or not self.data.get('bom'):
115113
return False
116114
return True
117115

@@ -122,46 +120,56 @@ def _get_bom(self):
122120
dict: If using scanoss.json
123121
list: If using SBOM.json
124122
"""
125-
if self.settings_file_type == "legacy":
123+
if self.settings_file_type == 'legacy':
126124
if isinstance(self.data, list):
127125
return self.data
128-
elif isinstance(self.data, dict) and self.data.get("components"):
129-
return self.data.get("components")
126+
elif isinstance(self.data, dict) and self.data.get('components'):
127+
return self.data.get('components')
130128
else:
131129
return []
132-
return self.data.get("bom", {})
130+
return self.data.get('bom', {})
133131

134132
def get_bom_include(self) -> List[BomEntry]:
135133
"""Get the list of components to include in the scan
136134
137135
Returns:
138136
list: List of components to include in the scan
139137
"""
140-
if self.settings_file_type == "legacy":
138+
if self.settings_file_type == 'legacy':
141139
return self._get_bom()
142-
return self._get_bom().get("include", [])
140+
return self._get_bom().get('include', [])
143141

144142
def get_bom_remove(self) -> List[BomEntry]:
145143
"""Get the list of components to remove from the scan
146144
147145
Returns:
148146
list: List of components to remove from the scan
149147
"""
150-
if self.settings_file_type == "legacy":
148+
if self.settings_file_type == 'legacy':
151149
return self._get_bom()
152-
return self._get_bom().get("remove", [])
150+
return self._get_bom().get('remove', [])
151+
152+
def get_bom_replace(self) -> List[BomEntry]:
153+
"""Get the list of components to replace in the scan
154+
155+
Returns:
156+
list: List of components to replace in the scan
157+
"""
158+
if self.settings_file_type == 'legacy':
159+
return []
160+
return self._get_bom().get('replace', [])
153161

154162
def get_sbom(self):
155163
"""Get the SBOM to be sent to the SCANOSS API
156164
157165
Returns:
158-
dict: SBOM
166+
dict: SBOM request payload
159167
"""
160168
if not self.data:
161169
return None
162170
return {
163-
"scan_type": self.scan_type,
164-
"assets": json.dumps(self._get_sbom_assets()),
171+
'scan_type': self.scan_type,
172+
'assets': json.dumps(self._get_sbom_assets()),
165173
}
166174

167175
def _get_sbom_assets(self):
@@ -170,8 +178,15 @@ def _get_sbom_assets(self):
170178
Returns:
171179
List: List of SBOM assets
172180
"""
173-
if self.scan_type == "identify":
174-
return self.normalize_bom_entries(self.get_bom_include())
181+
if self.scan_type == 'identify':
182+
include_bom_entries = self._remove_duplicates(self.normalize_bom_entries(self.get_bom_include()))
183+
replace_bom_entries = self._remove_duplicates(self.normalize_bom_entries(self.get_bom_replace()))
184+
self.print_debug(
185+
f"Scan type set to 'identify'. Adding {len(include_bom_entries) + len(replace_bom_entries)} components as context to the scan. \n"
186+
f"From Include list: {[entry['purl'] for entry in include_bom_entries]} \n"
187+
f"From Replace list: {[entry['purl'] for entry in replace_bom_entries]} \n"
188+
)
189+
return include_bom_entries + replace_bom_entries
175190
return self.normalize_bom_entries(self.get_bom_remove())
176191

177192
@staticmethod
@@ -188,7 +203,26 @@ def normalize_bom_entries(bom_entries) -> List[BomEntry]:
188203
for entry in bom_entries:
189204
normalized_bom_entries.append(
190205
{
191-
"purl": entry.get("purl", ""),
206+
'purl': entry.get('purl', ''),
192207
}
193208
)
194209
return normalized_bom_entries
210+
211+
@staticmethod
212+
def _remove_duplicates(bom_entries: List[BomEntry]) -> List[BomEntry]:
213+
"""Remove duplicate BOM entries
214+
215+
Args:
216+
bom_entries (List[Dict]): List of BOM entries
217+
218+
Returns:
219+
List: List of unique BOM entries
220+
"""
221+
already_added = set()
222+
unique_entries = []
223+
for entry in bom_entries:
224+
entry_tuple = tuple(entry.items())
225+
if entry_tuple not in already_added:
226+
already_added.add(entry_tuple)
227+
unique_entries.append(entry)
228+
return unique_entries

0 commit comments

Comments
 (0)