scanoss 1.17.5__tar.gz → 1.18.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. {scanoss-1.17.5/src/scanoss.egg-info → scanoss-1.18.0}/PKG-INFO +2 -1
  2. {scanoss-1.17.5 → scanoss-1.18.0}/README.md +16 -0
  3. {scanoss-1.17.5 → scanoss-1.18.0}/setup.cfg +1 -0
  4. {scanoss-1.17.5 → scanoss-1.18.0}/src/scanoss/__init__.py +1 -1
  5. {scanoss-1.17.5 → scanoss-1.18.0}/src/scanoss/cli.py +3 -1
  6. scanoss-1.18.0/src/scanoss/data/build_date.txt +1 -0
  7. {scanoss-1.17.5 → scanoss-1.18.0}/src/scanoss/inspection/undeclared_component.py +41 -11
  8. {scanoss-1.17.5 → scanoss-1.18.0}/src/scanoss/scanner.py +10 -8
  9. {scanoss-1.17.5 → scanoss-1.18.0}/src/scanoss/scanoss_settings.py +56 -22
  10. scanoss-1.18.0/src/scanoss/scanpostprocessor.py +283 -0
  11. {scanoss-1.17.5 → scanoss-1.18.0/src/scanoss.egg-info}/PKG-INFO +2 -1
  12. {scanoss-1.17.5 → scanoss-1.18.0}/src/scanoss.egg-info/SOURCES.txt +5 -1
  13. {scanoss-1.17.5 → scanoss-1.18.0}/src/scanoss.egg-info/requires.txt +1 -0
  14. scanoss-1.18.0/tests/test_csv_output.py +48 -0
  15. scanoss-1.18.0/tests/test_policy_inspect.py +336 -0
  16. scanoss-1.18.0/tests/test_scan_post_processor.py +118 -0
  17. scanoss-1.18.0/tests/test_winnowing.py +81 -0
  18. scanoss-1.17.5/src/scanoss/data/build_date.txt +0 -1
  19. scanoss-1.17.5/src/scanoss/scanpostprocessor.py +0 -159
  20. {scanoss-1.17.5 → scanoss-1.18.0}/LICENSE +0 -0
  21. {scanoss-1.17.5 → scanoss-1.18.0}/PACKAGE.md +0 -0
  22. {scanoss-1.17.5 → scanoss-1.18.0}/pyproject.toml +0 -0
  23. {scanoss-1.17.5 → scanoss-1.18.0}/src/protoc_gen_swagger/__init__.py +0 -0
  24. {scanoss-1.17.5 → scanoss-1.18.0}/src/protoc_gen_swagger/options/__init__.py +0 -0
  25. {scanoss-1.17.5 → scanoss-1.18.0}/src/protoc_gen_swagger/options/annotations_pb2.py +0 -0
  26. {scanoss-1.17.5 → scanoss-1.18.0}/src/protoc_gen_swagger/options/annotations_pb2_grpc.py +0 -0
  27. {scanoss-1.17.5 → scanoss-1.18.0}/src/protoc_gen_swagger/options/openapiv2_pb2.py +0 -0
  28. {scanoss-1.17.5 → scanoss-1.18.0}/src/protoc_gen_swagger/options/openapiv2_pb2_grpc.py +0 -0
  29. {scanoss-1.17.5 → scanoss-1.18.0}/src/scanoss/api/__init__.py +0 -0
  30. {scanoss-1.17.5 → scanoss-1.18.0}/src/scanoss/api/common/__init__.py +0 -0
  31. {scanoss-1.17.5 → scanoss-1.18.0}/src/scanoss/api/common/v2/__init__.py +0 -0
  32. {scanoss-1.17.5 → scanoss-1.18.0}/src/scanoss/api/common/v2/scanoss_common_pb2.py +0 -0
  33. {scanoss-1.17.5 → scanoss-1.18.0}/src/scanoss/api/common/v2/scanoss_common_pb2_grpc.py +0 -0
  34. {scanoss-1.17.5 → scanoss-1.18.0}/src/scanoss/api/components/__init__.py +0 -0
  35. {scanoss-1.17.5 → scanoss-1.18.0}/src/scanoss/api/components/v2/__init__.py +0 -0
  36. {scanoss-1.17.5 → scanoss-1.18.0}/src/scanoss/api/components/v2/scanoss_components_pb2.py +0 -0
  37. {scanoss-1.17.5 → scanoss-1.18.0}/src/scanoss/api/components/v2/scanoss_components_pb2_grpc.py +0 -0
  38. {scanoss-1.17.5 → scanoss-1.18.0}/src/scanoss/api/cryptography/v2/scanoss_cryptography_pb2.py +0 -0
  39. {scanoss-1.17.5 → scanoss-1.18.0}/src/scanoss/api/cryptography/v2/scanoss_cryptography_pb2_grpc.py +0 -0
  40. {scanoss-1.17.5 → scanoss-1.18.0}/src/scanoss/api/dependencies/__init__.py +0 -0
  41. {scanoss-1.17.5 → scanoss-1.18.0}/src/scanoss/api/dependencies/v2/__init__.py +0 -0
  42. {scanoss-1.17.5 → scanoss-1.18.0}/src/scanoss/api/dependencies/v2/scanoss_dependencies_pb2.py +0 -0
  43. {scanoss-1.17.5 → scanoss-1.18.0}/src/scanoss/api/dependencies/v2/scanoss_dependencies_pb2_grpc.py +0 -0
  44. {scanoss-1.17.5 → scanoss-1.18.0}/src/scanoss/api/scanning/__init__.py +0 -0
  45. {scanoss-1.17.5 → scanoss-1.18.0}/src/scanoss/api/scanning/v2/__init__.py +0 -0
  46. {scanoss-1.17.5 → scanoss-1.18.0}/src/scanoss/api/scanning/v2/scanoss_scanning_pb2.py +0 -0
  47. {scanoss-1.17.5 → scanoss-1.18.0}/src/scanoss/api/scanning/v2/scanoss_scanning_pb2_grpc.py +0 -0
  48. {scanoss-1.17.5 → scanoss-1.18.0}/src/scanoss/api/semgrep/__init__.py +0 -0
  49. {scanoss-1.17.5 → scanoss-1.18.0}/src/scanoss/api/semgrep/v2/__init__.py +0 -0
  50. {scanoss-1.17.5 → scanoss-1.18.0}/src/scanoss/api/semgrep/v2/scanoss_semgrep_pb2.py +0 -0
  51. {scanoss-1.17.5 → scanoss-1.18.0}/src/scanoss/api/semgrep/v2/scanoss_semgrep_pb2_grpc.py +0 -0
  52. {scanoss-1.17.5 → scanoss-1.18.0}/src/scanoss/api/vulnerabilities/__init__.py +0 -0
  53. {scanoss-1.17.5 → scanoss-1.18.0}/src/scanoss/api/vulnerabilities/v2/__init__.py +0 -0
  54. {scanoss-1.17.5 → scanoss-1.18.0}/src/scanoss/api/vulnerabilities/v2/scanoss_vulnerabilities_pb2.py +0 -0
  55. {scanoss-1.17.5 → scanoss-1.18.0}/src/scanoss/api/vulnerabilities/v2/scanoss_vulnerabilities_pb2_grpc.py +0 -0
  56. {scanoss-1.17.5 → scanoss-1.18.0}/src/scanoss/components.py +0 -0
  57. {scanoss-1.17.5 → scanoss-1.18.0}/src/scanoss/csvoutput.py +0 -0
  58. {scanoss-1.17.5 → scanoss-1.18.0}/src/scanoss/cyclonedx.py +0 -0
  59. {scanoss-1.17.5 → scanoss-1.18.0}/src/scanoss/data/spdx-exceptions.json +0 -0
  60. {scanoss-1.17.5 → scanoss-1.18.0}/src/scanoss/data/spdx-licenses.json +0 -0
  61. {scanoss-1.17.5 → scanoss-1.18.0}/src/scanoss/filecount.py +0 -0
  62. {scanoss-1.17.5 → scanoss-1.18.0}/src/scanoss/inspection/__init__.py +0 -0
  63. {scanoss-1.17.5 → scanoss-1.18.0}/src/scanoss/inspection/copyleft.py +0 -0
  64. {scanoss-1.17.5 → scanoss-1.18.0}/src/scanoss/inspection/policy_check.py +0 -0
  65. {scanoss-1.17.5 → scanoss-1.18.0}/src/scanoss/inspection/utils/license_utils.py +0 -0
  66. {scanoss-1.17.5 → scanoss-1.18.0}/src/scanoss/results.py +0 -0
  67. {scanoss-1.17.5 → scanoss-1.18.0}/src/scanoss/scancodedeps.py +0 -0
  68. {scanoss-1.17.5 → scanoss-1.18.0}/src/scanoss/scanossapi.py +0 -0
  69. {scanoss-1.17.5 → scanoss-1.18.0}/src/scanoss/scanossbase.py +0 -0
  70. {scanoss-1.17.5 → scanoss-1.18.0}/src/scanoss/scanossgrpc.py +0 -0
  71. {scanoss-1.17.5 → scanoss-1.18.0}/src/scanoss/scantype.py +0 -0
  72. {scanoss-1.17.5 → scanoss-1.18.0}/src/scanoss/spdxlite.py +0 -0
  73. {scanoss-1.17.5 → scanoss-1.18.0}/src/scanoss/threadeddependencies.py +0 -0
  74. {scanoss-1.17.5 → scanoss-1.18.0}/src/scanoss/threadedscanning.py +0 -0
  75. {scanoss-1.17.5 → scanoss-1.18.0}/src/scanoss/winnowing.py +0 -0
  76. {scanoss-1.17.5 → scanoss-1.18.0}/src/scanoss.egg-info/dependency_links.txt +0 -0
  77. {scanoss-1.17.5 → scanoss-1.18.0}/src/scanoss.egg-info/entry_points.txt +0 -0
  78. {scanoss-1.17.5 → scanoss-1.18.0}/src/scanoss.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: scanoss
3
- Version: 1.17.5
3
+ Version: 1.18.0
4
4
  Summary: Simple Python library to leverage the SCANOSS APIs
5
5
  Home-page: https://scanoss.com
6
6
  Author: SCANOSS
@@ -26,6 +26,7 @@ Requires-Dist: pypac
26
26
  Requires-Dist: pyOpenSSL
27
27
  Requires-Dist: google-api-core
28
28
  Requires-Dist: importlib_resources
29
+ Requires-Dist: packageurl-python
29
30
  Provides-Extra: fast-winnowing
30
31
  Requires-Dist: scanoss_winnowing>=0.5.0; extra == "fast-winnowing"
31
32
 
@@ -39,6 +39,22 @@ To enable dependency scanning, an extra tool is required: scancode-toolkit
39
39
  pip3 install -r requirements-scancode.txt
40
40
  ```
41
41
 
42
+ ### Devcontainer Setup
43
+ To simplify the development environment setup, a devcontainer configuration is provided. This allows you to develop inside a containerized environment with all necessary dependencies pre-installed.
44
+
45
+ To use the devcontainer setup:
46
+ 1. Install [Visual Studio Code](https://code.visualstudio.com/).
47
+ 2. Install the [Remote - Containers](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers) extension.
48
+ 3. Open the project in Visual Studio Code.
49
+ 4. Run
50
+ ```bash
51
+ cp .devcontainer/devcontainer.example.json .devcontainer/devcontainer.json
52
+ ```
53
+ 5. Update the `devcontainer.json` file with the desired settings.
54
+ 6. When prompted, reopen the project in the container.
55
+
56
+ This will build the container defined in the `.devcontainer` folder and open a new Visual Studio Code window connected to the container.
57
+
42
58
  ### Package Development
43
59
  More details on Python packaging/distribution can be found [here](https://packaging.python.org/overview/), [here](https://packaging.python.org/guides/distributing-packages-using-setuptools/), and [here](https://packaging.python.org/guides/using-testpypi/#using-test-pypi).
44
60
 
@@ -35,6 +35,7 @@ install_requires =
35
35
  pyOpenSSL
36
36
  google-api-core
37
37
  importlib_resources
38
+ packageurl-python
38
39
 
39
40
  [options.extras_require]
40
41
  fast_winnowing =
@@ -22,4 +22,4 @@
22
22
  THE SOFTWARE.
23
23
  """
24
24
 
25
- __version__ = '1.17.5'
25
+ __version__ = "1.18.0"
@@ -315,6 +315,8 @@ def setup_args() -> None:
315
315
 
316
316
  # Inspect Sub-command: inspect undeclared
317
317
  p_undeclared = p_inspect_sub.add_parser('undeclared', aliases=['un'],description="Inspect for undeclared components", help='Inspect for undeclared components')
318
+ p_undeclared.add_argument('--sbom-format',required=False ,choices=['legacy', 'settings'],
319
+ default="settings",help='Sbom format for status output')
318
320
  p_undeclared.set_defaults(func=inspect_undeclared)
319
321
 
320
322
  for p in [p_copyleft, p_undeclared]:
@@ -858,7 +860,7 @@ def inspect_undeclared(parser, args):
858
860
  open(status_output, 'w').close()
859
861
  i_undeclared = UndeclaredComponent(debug=args.debug, trace=args.trace, quiet=args.quiet,
860
862
  filepath=args.input, format_type=args.format,
861
- status=status_output, output=output)
863
+ status=status_output, output=output, sbom_format=args.sbom_format)
862
864
  status, _ = i_undeclared.run()
863
865
  sys.exit(status)
864
866
 
@@ -0,0 +1 @@
1
+ date: 20241115114610, utime: 1731671170
@@ -32,7 +32,7 @@ class UndeclaredComponent(PolicyCheck):
32
32
  """
33
33
 
34
34
  def __init__(self, debug: bool = False, trace: bool = True, quiet: bool = False, filepath: str = None,
35
- format_type: str = 'json', status: str = None, output: str = None):
35
+ format_type: str = 'json', status: str = None, output: str = None, sbom_format: str = 'settings'):
36
36
  """
37
37
  Initialize the UndeclaredComponent class.
38
38
 
@@ -43,6 +43,7 @@ class UndeclaredComponent(PolicyCheck):
43
43
  :param format_type: Output format ('json' or 'md')
44
44
  :param status: Path to save status output
45
45
  :param output: Path to save detailed output
46
+ :param sbom_format: Sbom format for status output (default 'settings')
46
47
  """
47
48
  super().__init__(debug, trace, quiet, filepath, format_type, status, output,
48
49
  name='Undeclared Components Policy')
@@ -50,6 +51,7 @@ class UndeclaredComponent(PolicyCheck):
50
51
  self.format = format
51
52
  self.output = output
52
53
  self.status = status
54
+ self.sbom_format = sbom_format
53
55
 
54
56
  def _get_undeclared_component(self, components: list)-> list or None:
55
57
  """
@@ -59,7 +61,7 @@ class UndeclaredComponent(PolicyCheck):
59
61
  :return: List of undeclared components
60
62
  """
61
63
  if components is None:
62
- self.print_stderr(f'WARNING: No components provided!')
64
+ self.print_debug(f'WARNING: No components provided!')
63
65
  return None
64
66
  undeclared_components = []
65
67
  for component in components:
@@ -78,9 +80,14 @@ class UndeclaredComponent(PolicyCheck):
78
80
  """
79
81
  summary = f'{len(components)} undeclared component(s) were found.\n'
80
82
  if len(components) > 0:
83
+ if self.sbom_format == 'settings':
84
+ summary += (f'Add the following snippet into your `scanoss.json` file\n'
85
+ f'\n```json\n{json.dumps(self._generate_scanoss_file(components), indent=2)}\n```\n')
86
+ return summary
87
+
81
88
  summary += (f'Add the following snippet into your `sbom.json` file\n'
82
89
  f'\n```json\n{json.dumps(self._generate_sbom_file(components), indent=2)}\n```\n')
83
- return summary
90
+ return summary
84
91
 
85
92
  def _json(self, components: list) -> Dict[str, Any]:
86
93
  """
@@ -115,23 +122,46 @@ class UndeclaredComponent(PolicyCheck):
115
122
  'summary': self._get_summary(components),
116
123
  }
117
124
 
118
- def _generate_sbom_file(self, components: list) -> dict:
125
+ def _get_unique_components(self, components: list) -> list:
119
126
  """
120
- Generate a list of PURLs for the SBOM file.
127
+ Generate a list of unique components.
121
128
 
122
129
  :param components: List of undeclared components
123
- :return: SBOM Dictionary with components
130
+ :return: list of unique components
124
131
  """
125
-
126
132
  unique_components = {}
127
133
  if components is None:
128
134
  self.print_stderr(f'WARNING: No components provided!')
129
- else:
130
- for component in components:
131
- unique_components[component['purl']] = { 'purl': component['purl'] }
135
+ return []
136
+
137
+ for component in components:
138
+ unique_components[component['purl']] = {'purl': component['purl']}
139
+ return list(unique_components.values())
140
+
141
+ def _generate_scanoss_file(self, components: list) -> dict:
142
+ """
143
+ Generate a list of PURLs for the scanoss.json file.
144
+
145
+ :param components: List of undeclared components
146
+ :return: scanoss.json Dictionary
147
+ """
148
+ scanoss_settings = {
149
+ 'bom':{
150
+ 'include': self._get_unique_components(components),
151
+ }
152
+ }
132
153
 
154
+ return scanoss_settings
155
+
156
+ def _generate_sbom_file(self, components: list) -> dict:
157
+ """
158
+ Generate a list of PURLs for the SBOM file.
159
+
160
+ :param components: List of undeclared components
161
+ :return: SBOM Dictionary with components
162
+ """
133
163
  sbom = {
134
- 'components': list(unique_components.values())
164
+ 'components': self._get_unique_components(components),
135
165
  }
136
166
 
137
167
  return sbom
@@ -161,12 +161,13 @@ class Scanner(ScanossBase):
161
161
  if skip_extensions: # Append extra file extensions to skip
162
162
  self.skip_extensions.extend(skip_extensions)
163
163
 
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()
168
167
 
169
168
  def _maybe_set_api_sbom(self):
169
+ if not self.scan_settings:
170
+ return
170
171
  sbom = self.scan_settings.get_sbom()
171
172
  if sbom:
172
173
  self.scanoss_api.set_sbom(sbom)
@@ -521,11 +522,12 @@ class Scanner(ScanossBase):
521
522
  success = False
522
523
  dep_responses = self.threaded_deps.responses
523
524
 
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)
527
526
 
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
529
531
 
530
532
  if self.output_format == 'plain':
531
533
  self.__log_result(json.dumps(results, indent=2, sort_keys=True))
@@ -69,15 +69,15 @@ class ScanossSettings(ScanossBase):
69
69
  json_file = Path(filepath).resolve()
70
70
 
71
71
  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}')
73
73
  self.data = {}
74
74
 
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}')
77
77
  try:
78
78
  self.data = json.load(jsonfile)
79
79
  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}')
81
81
  return self
82
82
 
83
83
  def set_file_type(self, file_type: str):
@@ -91,9 +91,7 @@ class ScanossSettings(ScanossBase):
91
91
  """
92
92
  self.settings_file_type = file_type
93
93
  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")')
97
95
  return self
98
96
 
99
97
  def set_scan_type(self, scan_type: str):
@@ -111,7 +109,7 @@ class ScanossSettings(ScanossBase):
111
109
  Returns:
112
110
  bool: True if the file is valid, False otherwise
113
111
  """
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'):
115
113
  return False
116
114
  return True
117
115
 
@@ -122,14 +120,14 @@ class ScanossSettings(ScanossBase):
122
120
  dict: If using scanoss.json
123
121
  list: If using SBOM.json
124
122
  """
125
- if self.settings_file_type == "legacy":
123
+ if self.settings_file_type == 'legacy':
126
124
  if isinstance(self.data, list):
127
125
  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')
130
128
  else:
131
129
  return []
132
- return self.data.get("bom", {})
130
+ return self.data.get('bom', {})
133
131
 
134
132
  def get_bom_include(self) -> List[BomEntry]:
135
133
  """Get the list of components to include in the scan
@@ -137,9 +135,9 @@ class ScanossSettings(ScanossBase):
137
135
  Returns:
138
136
  list: List of components to include in the scan
139
137
  """
140
- if self.settings_file_type == "legacy":
138
+ if self.settings_file_type == 'legacy':
141
139
  return self._get_bom()
142
- return self._get_bom().get("include", [])
140
+ return self._get_bom().get('include', [])
143
141
 
144
142
  def get_bom_remove(self) -> List[BomEntry]:
145
143
  """Get the list of components to remove from the scan
@@ -147,21 +145,31 @@ class ScanossSettings(ScanossBase):
147
145
  Returns:
148
146
  list: List of components to remove from the scan
149
147
  """
150
- if self.settings_file_type == "legacy":
148
+ if self.settings_file_type == 'legacy':
151
149
  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', [])
153
161
 
154
162
  def get_sbom(self):
155
163
  """Get the SBOM to be sent to the SCANOSS API
156
164
 
157
165
  Returns:
158
- dict: SBOM
166
+ dict: SBOM request payload
159
167
  """
160
168
  if not self.data:
161
169
  return None
162
170
  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()),
165
173
  }
166
174
 
167
175
  def _get_sbom_assets(self):
@@ -170,8 +178,15 @@ class ScanossSettings(ScanossBase):
170
178
  Returns:
171
179
  List: List of SBOM assets
172
180
  """
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
175
190
  return self.normalize_bom_entries(self.get_bom_remove())
176
191
 
177
192
  @staticmethod
@@ -188,7 +203,26 @@ class ScanossSettings(ScanossBase):
188
203
  for entry in bom_entries:
189
204
  normalized_bom_entries.append(
190
205
  {
191
- "purl": entry.get("purl", ""),
206
+ 'purl': entry.get('purl', ''),
192
207
  }
193
208
  )
194
209
  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,0 +1,283 @@
1
+ """
2
+ SPDX-License-Identifier: MIT
3
+
4
+ Copyright (c) 2024, SCANOSS
5
+
6
+ Permission is hereby granted, free of charge, to any person obtaining a copy
7
+ of this software and associated documentation files (the "Software"), to deal
8
+ in the Software without restriction, including without limitation the rights
9
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10
+ copies of the Software, and to permit persons to whom the Software is
11
+ furnished to do so, subject to the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be included in
14
+ all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22
+ THE SOFTWARE.
23
+ """
24
+
25
+ from typing import List, Tuple
26
+
27
+ from packageurl import PackageURL
28
+ from packageurl.contrib import purl2url
29
+
30
+ from .scanoss_settings import BomEntry, ScanossSettings
31
+ from .scanossbase import ScanossBase
32
+
33
+
34
+ class ScanPostProcessor(ScanossBase):
35
+ """Handles post-processing of the scan results"""
36
+
37
+ def __init__(
38
+ self,
39
+ scan_settings: ScanossSettings,
40
+ debug: bool = False,
41
+ trace: bool = False,
42
+ quiet: bool = False,
43
+ results: dict = None,
44
+ ):
45
+ """
46
+ Args:
47
+ scan_settings (ScanossSettings): Scan settings object
48
+ debug (bool, optional): Debug mode. Defaults to False.
49
+ trace (bool, optional): Traces. Defaults to False.
50
+ quiet (bool, optional): Quiet mode. Defaults to False.
51
+ results (dict | str, optional): Results to be processed. Defaults to None.
52
+ """
53
+ super().__init__(debug, trace, quiet)
54
+ self.scan_settings = scan_settings
55
+ self.results: dict = results
56
+ self.component_info_map: dict = {}
57
+
58
+ def load_results(self, raw_results: dict):
59
+ """Load the raw results
60
+
61
+ Args:
62
+ raw_results (dict): Raw scan results
63
+ """
64
+ self.results = raw_results
65
+ self._load_component_info()
66
+ return self
67
+
68
+ def _load_component_info(self):
69
+ """Create a map of component information from scan results for faster lookup"""
70
+ if not self.results:
71
+ return
72
+ for _, result in self.results.items():
73
+ result = result[0] if isinstance(result, list) else result
74
+ purls = result.get('purl', [])
75
+ for purl in purls:
76
+ self.component_info_map[purl] = result
77
+
78
+ def post_process(self):
79
+ """Post-process the scan results
80
+
81
+ Returns:
82
+ dict: Processed results
83
+ """
84
+ self._remove_dismissed_files()
85
+ self._replace_purls()
86
+ return self.results
87
+
88
+ def _remove_dismissed_files(self):
89
+ """Remove entries from the results based on files and/or purls specified in the SCANOSS settings file"""
90
+ to_remove_entries = self.scan_settings.get_bom_remove()
91
+ if not to_remove_entries:
92
+ return
93
+
94
+ self.results = {
95
+ result_path: result
96
+ for result_path, result in self.results.items()
97
+ if not self._should_remove_result(result_path, result, to_remove_entries)
98
+ }
99
+
100
+ def _replace_purls(self):
101
+ """Replace purls in the results based on the SCANOSS settings file"""
102
+ to_replace_entries = self.scan_settings.get_bom_replace()
103
+ if not to_replace_entries:
104
+ return
105
+
106
+ for result_path, result in self.results.items():
107
+ result = result[0] if isinstance(result, list) else result
108
+ should_replace, to_replace_with_purl = self._should_replace_result(result_path, result, to_replace_entries)
109
+ if should_replace:
110
+ self.results[result_path] = [self._update_replaced_result(result, to_replace_with_purl)]
111
+
112
+ def _update_replaced_result(self, result: dict, to_replace_with_purl: str) -> dict:
113
+ """
114
+ Update the result with the new purl and component information if available,
115
+ otherwise removes the old component information
116
+
117
+ Args:
118
+ result (dict): The result to update
119
+ to_replace_with_purl (str): The purl to replace with
120
+
121
+ Returns:
122
+ dict: Updated result
123
+ """
124
+
125
+ if self.component_info_map.get(to_replace_with_purl):
126
+ result.update(self.component_info_map[to_replace_with_purl])
127
+ else:
128
+ try:
129
+ new_component = PackageURL.from_string(to_replace_with_purl).to_dict()
130
+ new_component_url = purl2url.get_repo_url(to_replace_with_purl)
131
+ except Exception:
132
+ self.print_stderr(
133
+ f"Error while replacing: Invalid PURL '{to_replace_with_purl}' in settings file. Abort replacing."
134
+ )
135
+ return result
136
+
137
+ result['component'] = new_component.get('name')
138
+ result['url'] = new_component_url
139
+ result['vendor'] = new_component.get('namespace')
140
+
141
+ result.pop('licenses', None)
142
+ result.pop('file', None)
143
+ result.pop('file_hash', None)
144
+ result.pop('file_url', None)
145
+ result.pop('latest', None)
146
+ result.pop('release_date', None)
147
+ result.pop('source_hash', None)
148
+ result.pop('url_hash', None)
149
+ result.pop('url_stats', None)
150
+ result.pop('url_stats', None)
151
+ result.pop('version', None)
152
+
153
+ result['purl'] = [to_replace_with_purl]
154
+
155
+ return result
156
+
157
+ def _should_replace_result(
158
+ self, result_path: str, result: dict, to_replace_entries: List[BomEntry]
159
+ ) -> Tuple[bool, str]:
160
+ """Check if a result should be replaced based on the SCANOSS settings
161
+
162
+ Args:
163
+ result_path (str): Path of the result
164
+ result (dict): Result to check
165
+ to_replace_entries (List[BomEntry]): BOM entries to replace from the settings file
166
+
167
+ Returns:
168
+ bool: True if the result should be replaced, False otherwise
169
+ str: The purl to replace with
170
+ """
171
+ result_purls = result.get('purl', [])
172
+ for to_replace_entry in to_replace_entries:
173
+ to_replace_path = to_replace_entry.get('path')
174
+ to_replace_purl = to_replace_entry.get('purl')
175
+ to_replace_with = to_replace_entry.get('replace_with')
176
+
177
+ if not to_replace_path and not to_replace_purl or not to_replace_with:
178
+ continue
179
+
180
+ if (
181
+ self._is_full_match(result_path, result_purls, to_replace_entry)
182
+ or (not to_replace_path and to_replace_purl in result_purls)
183
+ or (not to_replace_purl and to_replace_path == result_path)
184
+ ):
185
+ self._print_message(result_path, result_purls, to_replace_entry, 'Replacing')
186
+ return True, to_replace_with
187
+
188
+ return False, None
189
+
190
+ def _should_remove_result(self, result_path: str, result: dict, to_remove_entries: List[BomEntry]) -> bool:
191
+ """Check if a result should be removed based on the SCANOSS settings"""
192
+ result = result[0] if isinstance(result, list) else result
193
+ result_purls = result.get('purl', [])
194
+
195
+ for to_remove_entry in to_remove_entries:
196
+ to_remove_path = to_remove_entry.get('path')
197
+ to_remove_purl = to_remove_entry.get('purl')
198
+
199
+ if not to_remove_path and not to_remove_purl:
200
+ continue
201
+
202
+ if (
203
+ self._is_full_match(result_path, result_purls, to_remove_entry)
204
+ or (not to_remove_path and to_remove_purl in result_purls)
205
+ or (not to_remove_purl and to_remove_path == result_path)
206
+ ):
207
+ self._print_message(result_path, result_purls, to_remove_entry, 'Removing')
208
+ return True
209
+
210
+ return False
211
+
212
+ def _print_message(
213
+ self,
214
+ result_path: str,
215
+ result_purls: List[str],
216
+ bom_entry: BomEntry,
217
+ action: str,
218
+ ) -> None:
219
+ """Print a message about replacing or removing a result"""
220
+ message = (
221
+ f"{self._get_match_type_message(result_path, result_purls, bom_entry, action)} \n"
222
+ f"Details:\n"
223
+ f" - PURLs: {', '.join(result_purls)}\n"
224
+ f" - Path: '{result_path}'\n"
225
+ )
226
+
227
+ if action == 'Replacing':
228
+ message += f" - {action} with '{bom_entry.get('replace_with')}'"
229
+
230
+ self.print_debug(message)
231
+
232
+ def _get_match_type_message(
233
+ self,
234
+ result_path: str,
235
+ result_purls: List[str],
236
+ bom_entry: BomEntry,
237
+ action: str,
238
+ ) -> str:
239
+ """Compose message based on match type
240
+
241
+ Args:
242
+ result_path (str): Path of the scan result
243
+ result_purls (List[str]): Purls of the scan result
244
+ bom_entry (BomEntry): BOM entry to compare with
245
+ action (str): Post processing action being performed
246
+
247
+ Returns:
248
+ str: The message to be printed
249
+ """
250
+ if bom_entry.get('path') and bom_entry.get('purl'):
251
+ message = f"{action} '{result_path}'. Full match found."
252
+ elif bom_entry.get('purl'):
253
+ message = f"{action} '{result_path}'. Found PURL match."
254
+ else:
255
+ message = f"{action} '{result_path}'. Found path match."
256
+
257
+ return message
258
+
259
+ def _is_full_match(
260
+ self,
261
+ result_path: str,
262
+ result_purls: List[str],
263
+ bom_entry: BomEntry,
264
+ ) -> bool:
265
+ """Check if path and purl matches fully with the bom entry
266
+
267
+ Args:
268
+ result_path (str): Scan result path
269
+ result_purls (List[str]): Scan result purls
270
+ bom_entry (BomEntry): BOM entry to compare with
271
+
272
+ Returns:
273
+ bool: True if the path and purl match, False otherwise
274
+ """
275
+
276
+ if not result_purls:
277
+ return False
278
+
279
+ return bool(
280
+ (bom_entry.get('purl') and bom_entry.get('path'))
281
+ and (bom_entry.get('path') == result_path)
282
+ and (bom_entry.get('purl') in result_purls)
283
+ )
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: scanoss
3
- Version: 1.17.5
3
+ Version: 1.18.0
4
4
  Summary: Simple Python library to leverage the SCANOSS APIs
5
5
  Home-page: https://scanoss.com
6
6
  Author: SCANOSS
@@ -26,6 +26,7 @@ Requires-Dist: pypac
26
26
  Requires-Dist: pyOpenSSL
27
27
  Requires-Dist: google-api-core
28
28
  Requires-Dist: importlib_resources
29
+ Requires-Dist: packageurl-python
29
30
  Provides-Extra: fast-winnowing
30
31
  Requires-Dist: scanoss_winnowing>=0.5.0; extra == "fast-winnowing"
31
32