scanoss 1.38.0__py3-none-any.whl → 1.39.0__py3-none-any.whl

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.
scanoss/__init__.py CHANGED
@@ -22,4 +22,4 @@ SPDX-License-Identifier: MIT
22
22
  THE SOFTWARE.
23
23
  """
24
24
 
25
- __version__ = '1.38.0'
25
+ __version__ = '1.39.0'
scanoss/cli.py CHANGED
@@ -40,6 +40,7 @@ from scanoss.inspection.dependency_track.project_violation import (
40
40
  )
41
41
  from scanoss.inspection.raw.component_summary import ComponentSummary
42
42
  from scanoss.inspection.raw.license_summary import LicenseSummary
43
+ from scanoss.inspection.raw.match_summary import MatchSummary
43
44
  from scanoss.scanners.container_scanner import (
44
45
  DEFAULT_SYFT_COMMAND,
45
46
  DEFAULT_SYFT_TIMEOUT,
@@ -73,6 +74,7 @@ from .constants import (
73
74
  from .csvoutput import CsvOutput
74
75
  from .cyclonedx import CycloneDx
75
76
  from .filecount import FileCount
77
+ from .gitlabqualityreport import GitLabQualityReport
76
78
  from .inspection.raw.copyleft import Copyleft
77
79
  from .inspection.raw.undeclared_component import UndeclaredComponent
78
80
  from .results import Results
@@ -283,7 +285,7 @@ def setup_args() -> None: # noqa: PLR0912, PLR0915
283
285
  '--format',
284
286
  '-f',
285
287
  type=str,
286
- choices=['cyclonedx', 'spdxlite', 'csv'],
288
+ choices=['cyclonedx', 'spdxlite', 'csv', 'glc-codequality'],
287
289
  default='spdxlite',
288
290
  help='Output format (optional - default: spdxlite)',
289
291
  )
@@ -794,6 +796,66 @@ def setup_args() -> None: # noqa: PLR0912, PLR0915
794
796
  help='Timeout (in seconds) for API communication (optional - default 300 sec)',
795
797
  )
796
798
 
799
+
800
+ # ==============================================================================
801
+ # GitLab Integration Parser
802
+ # ==============================================================================
803
+ # Main parser for GitLab-specific inspection commands and report generation
804
+ p_gitlab_sub = p_inspect_sub.add_parser(
805
+ 'gitlab',
806
+ aliases=['glc'],
807
+ description='Generate GitLab-compatible reports from SCANOSS scan results (Markdown summaries)',
808
+ help='Generate GitLab integration reports',
809
+ )
810
+
811
+ # GitLab sub-commands parser
812
+ # Provides access to different GitLab report formats and inspection tools
813
+ p_gitlab_sub_parser = p_gitlab_sub.add_subparsers(
814
+ title='GitLab Report Types',
815
+ dest='subparser_subcmd',
816
+ description='Available GitLab report formats for scan result analysis',
817
+ help='Select the type of GitLab report to generate',
818
+ )
819
+
820
+ # ==============================================================================
821
+ # GitLab Matches Summary Command
822
+ # ==============================================================================
823
+ # Analyzes scan results and generates a GitLab-compatible Markdown summary
824
+ p_gl_inspect_matches = p_gitlab_sub_parser.add_parser(
825
+ 'matches',
826
+ aliases=['ms'],
827
+ description='Generate a Markdown summary report of scan matches for GitLab integration',
828
+ help='Generate Markdown summary report of scan matches',
829
+ )
830
+
831
+ # Input file argument - SCANOSS scan results in JSON format
832
+ p_gl_inspect_matches.add_argument(
833
+ '-i',
834
+ '--input',
835
+ required=True,
836
+ type=str,
837
+ help='Path to SCANOSS scan results file (JSON format) to analyze'
838
+ )
839
+
840
+ # Line range prefix for GitLab file navigation
841
+ # Enables clickable file references in the generated report that link to specific lines in GitLab
842
+ p_gl_inspect_matches.add_argument(
843
+ '-lpr',
844
+ '--line-range-prefix',
845
+ required=True,
846
+ type=str,
847
+ help='Base URL prefix for GitLab file links with line ranges (e.g., https://gitlab.com/org/project/-/blob/main)'
848
+ )
849
+
850
+ # Output file argument - where to save the generated Markdown report
851
+ p_gl_inspect_matches.add_argument(
852
+ '--output',
853
+ '-o',
854
+ required=False,
855
+ type=str,
856
+ help='Output file path for the generated Markdown report (default: stdout)'
857
+ )
858
+
797
859
  # TODO Move to the command call def location
798
860
  # RAW results
799
861
  p_inspect_raw_undeclared.set_defaults(func=inspect_undeclared)
@@ -807,6 +869,8 @@ def setup_args() -> None: # noqa: PLR0912, PLR0915
807
869
  p_inspect_legacy_component_summary.set_defaults(func=inspect_component_summary)
808
870
  # Dependency Track
809
871
  p_inspect_dt_project_violation.set_defaults(func=inspect_dep_track_project_violations)
872
+ # GitLab
873
+ p_gl_inspect_matches.set_defaults(func=inspect_gitlab_matches)
810
874
 
811
875
  # =========================================================================
812
876
  # END INSPECT SUBCOMMAND CONFIGURATION
@@ -1153,6 +1217,7 @@ def setup_args() -> None: # noqa: PLR0912, PLR0915
1153
1217
  p_inspect_legacy_license_summary,
1154
1218
  p_inspect_legacy_component_summary,
1155
1219
  p_inspect_dt_project_violation,
1220
+ p_gl_inspect_matches,
1156
1221
  c_provenance,
1157
1222
  p_folder_scan,
1158
1223
  p_folder_hash,
@@ -1207,7 +1272,11 @@ def setup_args() -> None: # noqa: PLR0912, PLR0915
1207
1272
  ) and not args.subparsercmd:
1208
1273
  parser.parse_args([args.subparser, '--help']) # Force utils helps to be displayed
1209
1274
  sys.exit(1)
1210
- elif (args.subparser in 'inspect') and (args.subparsercmd in ('raw', 'dt')) and (args.subparser_subcmd is None):
1275
+ elif (
1276
+ (args.subparser in 'inspect')
1277
+ and (args.subparsercmd in ('raw', 'dt', 'glc', 'gitlab'))
1278
+ and (args.subparser_subcmd is None)
1279
+ ):
1211
1280
  parser.parse_args([args.subparser, args.subparsercmd, '--help']) # Force utils helps to be displayed
1212
1281
  sys.exit(1)
1213
1282
  args.func(parser, args) # Execute the function associated with the sub-command
@@ -1628,6 +1697,11 @@ def convert(parser, args):
1628
1697
  print_stderr('Producing CSV report...')
1629
1698
  csvo = CsvOutput(debug=args.debug, output_file=args.output)
1630
1699
  success = csvo.produce_from_file(args.input)
1700
+ elif args.format == 'glc-codequality':
1701
+ if not args.quiet:
1702
+ print_stderr('Producing GitLab code quality report...')
1703
+ glc_code_quality = GitLabQualityReport(debug=args.debug, trace=args.trace, quiet=args.quiet)
1704
+ success = glc_code_quality.produce_from_file(args.input, output_file=args.output)
1631
1705
  else:
1632
1706
  print_stderr(f'ERROR: Unknown output format (--format): {args.format}')
1633
1707
  if not success:
@@ -1901,6 +1975,69 @@ def inspect_dep_track_project_violations(parser, args):
1901
1975
  sys.exit(1)
1902
1976
 
1903
1977
 
1978
+ def inspect_gitlab_matches(parser,args):
1979
+ """
1980
+ Handle GitLab matches the summary inspection command.
1981
+
1982
+ Analyzes SCANOSS scan results and generates a GitLab-compatible Markdown summary
1983
+ report of component matches. The report includes match details, file locations,
1984
+ and optionally clickable links to source files in GitLab repositories.
1985
+
1986
+ This command processes SCANOSS scan output and creates human-readable Markdown.
1987
+
1988
+ Parameters
1989
+ ----------
1990
+ parser : ArgumentParser
1991
+ Command line parser object for help display
1992
+ args : Namespace
1993
+ Parsed command line arguments containing:
1994
+ - input: Path to SCANOSS scan results file (JSON format) to analyze
1995
+ - line_range_prefix: Base URL prefix for generating GitLab file links with line ranges
1996
+ (e.g., 'https://gitlab.com/org/project/-/blob/main')
1997
+ - output: Optional output file path for the generated Markdown report (default: stdout)
1998
+ - debug: Enable debug output for troubleshooting
1999
+ - trace: Enable trace-level logging
2000
+ - quiet: Suppress informational messages
2001
+
2002
+ Notes
2003
+ -----
2004
+ - The output is formatted in Markdown for optimal display in GitLab
2005
+ - Line range prefix enables clickable file references in the report
2006
+ - If output is not specified, the report is written to stdout
2007
+ """
2008
+
2009
+ if args.input is None:
2010
+ parser.parse_args([args.subparser, '-h'])
2011
+ sys.exit(1)
2012
+
2013
+ if args.line_range_prefix is None:
2014
+ parser.parse_args([args.subparser, '-h'])
2015
+ sys.exit(1)
2016
+
2017
+ # Initialize output file if specified (create/truncate)
2018
+ if args.output:
2019
+ initialise_empty_file(args.output)
2020
+
2021
+ try:
2022
+ # Create GitLab matches summary generator with configuration
2023
+ match_summary = MatchSummary(
2024
+ debug=args.debug,
2025
+ trace=args.trace,
2026
+ quiet=args.quiet,
2027
+ scanoss_results_path=args.input, # Path to SCANOSS JSON results
2028
+ output=args.output, # Output file path or None for stdout
2029
+ line_range_prefix=args.line_range_prefix, # GitLab URL prefix for file links
2030
+ )
2031
+
2032
+ # Execute the summary generation
2033
+ match_summary.run()
2034
+ except Exception as e:
2035
+ # Handle any errors during report generation
2036
+ print_stderr(e)
2037
+ if args.debug:
2038
+ traceback.print_exc()
2039
+ sys.exit(1)
2040
+
1904
2041
  # =============================================================================
1905
2042
  # END INSPECT COMMAND HANDLERS
1906
2043
  # =============================================================================
@@ -1 +1 @@
1
- date: 20251024131920, utime: 1761311960
1
+ date: 20251027150435, utime: 1761577475
@@ -0,0 +1,185 @@
1
+ """
2
+ SPDX-License-Identifier: MIT
3
+
4
+ Copyright (c) 2025, 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
+ import json
26
+ import os
27
+ import sys
28
+ from dataclasses import dataclass
29
+
30
+ from .scanossbase import ScanossBase
31
+ from .utils import scanoss_scan_results_utils
32
+
33
+
34
+ @dataclass
35
+ class Lines:
36
+ begin: int
37
+
38
+ @dataclass
39
+ class Location:
40
+ path: str
41
+ lines: Lines
42
+
43
+ @dataclass
44
+ class CodeQuality:
45
+ description: str
46
+ check_name: str
47
+ fingerprint: str
48
+ severity: str
49
+ location: Location
50
+
51
+ def to_dict(self):
52
+ """Convert to dictionary for JSON serialization."""
53
+ return {
54
+ "description": self.description,
55
+ "check_name": self.check_name,
56
+ "fingerprint": self.fingerprint,
57
+ "severity": self.severity,
58
+ "location": {
59
+ "path": self.location.path,
60
+ "lines": {
61
+ "begin": self.location.lines.begin
62
+ }
63
+ }
64
+ }
65
+
66
+ class GitLabQualityReport(ScanossBase):
67
+ """
68
+ GitLabCodeQuality management class
69
+ Handle all interaction with GitLab Code Quality Report formatting
70
+ """
71
+
72
+ def __init__(self, debug: bool = False, trace: bool = False, quiet: bool = False):
73
+ """
74
+ Initialise the GitLabCodeQuality class
75
+ """
76
+ super().__init__(debug, trace, quiet)
77
+
78
+
79
+ def _get_code_quality(self, file_name: str, result: dict) -> CodeQuality or None:
80
+ if not result.get('file_hash'):
81
+ self.print_debug(f"Warning: no hash found for result: {result}")
82
+ return None
83
+
84
+ if result.get('id') == 'file':
85
+ description = f"File match found in: {file_name}"
86
+ return CodeQuality(
87
+ description=description,
88
+ check_name=file_name,
89
+ fingerprint=result.get('file_hash'),
90
+ severity="info",
91
+ location=Location(
92
+ path=file_name,
93
+ lines = Lines(
94
+ begin= 1
95
+ )
96
+ )
97
+ )
98
+
99
+ if not result.get('lines'):
100
+ self.print_debug(f"Warning: No lines found for result: {result}")
101
+ return None
102
+ lines = scanoss_scan_results_utils.get_lines(result.get('lines'))
103
+ if len(lines) == 0:
104
+ self.print_debug(f"Warning: empty lines for result: {result}")
105
+ return None
106
+ end_line = lines[len(lines) - 1] if len(lines) > 1 else lines[0]
107
+ description = f"Snippet found in: {file_name} - lines {lines[0]}-{end_line}"
108
+ return CodeQuality(
109
+ description=description,
110
+ check_name=file_name,
111
+ fingerprint=result.get('file_hash'),
112
+ severity="info",
113
+ location=Location(
114
+ path=file_name,
115
+ lines=Lines(
116
+ begin=lines[0]
117
+ )
118
+ )
119
+ )
120
+
121
+ def _write_output(self, data: list[CodeQuality], output_file: str = None) -> bool:
122
+ """Write the Gitlab Code Quality Report to output."""
123
+ try:
124
+ json_data = [item.to_dict() for item in data]
125
+ file = open(output_file, 'w') if output_file else sys.stdout
126
+ print(json.dumps(json_data, indent=2), file=file)
127
+ if output_file:
128
+ file.close()
129
+ return True
130
+ except Exception as e:
131
+ self.print_stderr(f'Error writing output: {str(e)}')
132
+ return False
133
+
134
+ def _produce_from_json(self, data: dict, output_file: str = None) -> bool:
135
+ code_quality = []
136
+ for file_name, results in data.items():
137
+ for result in results:
138
+ if not result.get('id'):
139
+ self.print_debug(f"Warning: No ID found for result: {result}")
140
+ continue
141
+ if result.get('id') != 'snippet' and result.get('id') != 'file':
142
+ self.print_debug(f"Skipping non-snippet/file match: {result}")
143
+ continue
144
+ code_quality_item = self._get_code_quality(file_name, result)
145
+ if code_quality_item:
146
+ code_quality.append(code_quality_item)
147
+ else:
148
+ self.print_debug(f"Warning: No Code Quality found for result: {result}")
149
+ self._write_output(data=code_quality,output_file=output_file)
150
+ return True
151
+
152
+ def _produce_from_str(self, json_str: str, output_file: str = None) -> bool:
153
+ """
154
+ Produce Gitlab Code Quality Report output from input JSON string
155
+ :param json_str: input JSON string
156
+ :param output_file: Output file (optional)
157
+ :return: True if successful, False otherwise
158
+ """
159
+ if not json_str:
160
+ self.print_stderr('ERROR: No JSON string provided to parse.')
161
+ return False
162
+ try:
163
+ data = json.loads(json_str)
164
+ except Exception as e:
165
+ self.print_stderr(f'ERROR: Problem parsing input JSON: {e}')
166
+ return False
167
+ return self._produce_from_json(data, output_file)
168
+
169
+
170
+ def produce_from_file(self, json_file: str, output_file: str = None) -> bool:
171
+ """
172
+ Parse plain/raw input JSON file and produce GitLab Code Quality JSON output
173
+ :param json_file:
174
+ :param output_file:
175
+ :return: True if successful, False otherwise
176
+ """
177
+ if not json_file:
178
+ self.print_stderr('ERROR: No JSON file provided to parse.')
179
+ return False
180
+ if not os.path.isfile(json_file):
181
+ self.print_stderr(f'ERROR: JSON file does not exist or is not a file: {json_file}')
182
+ return False
183
+ with open(json_file, 'r') as f:
184
+ success = self._produce_from_str(f.read(), output_file)
185
+ return success
@@ -28,6 +28,7 @@ from typing import Any, Dict, List, Optional, TypedDict
28
28
 
29
29
  from ...services.dependency_track_service import DependencyTrackService
30
30
  from ..policy_check import PolicyCheck, PolicyStatus
31
+ from ..utils.markdown_utils import generate_jira_table, generate_table
31
32
 
32
33
  # Constants
33
34
  PROCESSING_RETRY_DELAY = 5 # seconds
@@ -195,7 +196,7 @@ class DependencyTrackProjectViolationPolicyCheck(PolicyCheck[PolicyViolationDict
195
196
  Returns:
196
197
  Dictionary with formatted Markdown details and summary
197
198
  """
198
- return self._md_summary_generator(project_violations, self.generate_table)
199
+ return self._md_summary_generator(project_violations, generate_table)
199
200
 
200
201
  def _jira_markdown(self, data: list[PolicyViolationDict]) -> Dict[str, Any]:
201
202
  """
@@ -207,7 +208,7 @@ class DependencyTrackProjectViolationPolicyCheck(PolicyCheck[PolicyViolationDict
207
208
  Returns:
208
209
  Dictionary containing Jira markdown formatted results and summary
209
210
  """
210
- return self._md_summary_generator(data, self.generate_jira_table)
211
+ return self._md_summary_generator(data, generate_jira_table)
211
212
 
212
213
  def is_project_updated(self, dt_project: Dict[str, Any]) -> bool:
213
214
  """
@@ -137,48 +137,6 @@ class PolicyCheck(ScanossBase, Generic[T]):
137
137
  """
138
138
  pass
139
139
 
140
- def generate_table(self, headers, rows, centered_columns=None):
141
- """
142
- Generate a Markdown table.
143
-
144
- :param headers: List of headers for the table.
145
- :param rows: List of rows for the table.
146
- :param centered_columns: List of column indices to be centered.
147
- :return: A string representing the Markdown table.
148
- """
149
- col_sep = ' | '
150
- centered_column_set = set(centered_columns or [])
151
- if headers is None:
152
- self.print_stderr('ERROR: Header are no set')
153
- return None
154
-
155
- # Decide which separator to use
156
- def create_separator(index):
157
- if centered_columns is None:
158
- return '-'
159
- return ':-:' if index in centered_column_set else '-'
160
-
161
- # Build the row separator
162
- row_separator = col_sep + col_sep.join(create_separator(index) for index, _ in enumerate(headers)) + col_sep
163
- # build table rows
164
- table_rows = [col_sep + col_sep.join(headers) + col_sep, row_separator]
165
- table_rows.extend(col_sep + col_sep.join(row) + col_sep for row in rows)
166
- return '\n'.join(table_rows)
167
-
168
- def generate_jira_table(self, headers, rows, centered_columns=None):
169
- col_sep = '*|*'
170
- if headers is None:
171
- self.print_stderr('ERROR: Header are no set')
172
- return None
173
-
174
- table_header = '|*' + col_sep.join(headers) + '*|\n'
175
- table = table_header
176
- for row in rows:
177
- if len(headers) == len(row):
178
- table += '|' + '|'.join(row) + '|\n'
179
-
180
- return table
181
-
182
140
  def _get_formatter(self) -> Callable[[List[dict]], Dict[str, Any]] or None:
183
141
  """
184
142
  Get the appropriate formatter function based on the specified format.
File without changes
@@ -21,13 +21,58 @@ SPDX-License-Identifier: MIT
21
21
  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22
22
  THE SOFTWARE.
23
23
  """
24
-
25
24
  import json
25
+ from typing import Any
26
26
 
27
+ from ..policy_check import T
27
28
  from .raw_base import RawBase
28
29
 
29
30
 
30
31
  class ComponentSummary(RawBase):
32
+
33
+ def _json(self, data: dict[str,Any]) -> dict[str,Any]:
34
+ """
35
+ Format component summary data as JSON.
36
+
37
+ This method returns the component summary data in its original JSON structure
38
+ without any transformation. The data can be directly serialized to JSON format.
39
+
40
+ :param data: Dictionary containing component summary information including:
41
+ - components: List of component-license pairs with status and metadata
42
+ - totalComponents: Total number of unique components
43
+ - undeclaredComponents: Number of components with 'pending' status
44
+ - declaredComponents: Number of components with 'identified' status
45
+ - totalFilesDetected: Total count of files where components were detected
46
+ - totalFilesUndeclared: Count of files with undeclared components
47
+ - totalFilesDeclared: Count of files with declared components
48
+ :return: The same data dictionary, ready for JSON serialization
49
+ """
50
+ return data
51
+
52
+ def _markdown(self, data: list[T]) -> dict[str, Any]:
53
+ """
54
+ Format component summary data as Markdown (not yet implemented).
55
+
56
+ This method is intended to convert component summary data into a human-readable
57
+ Markdown format with tables and formatted sections.
58
+
59
+ :param data: List of component summary items to format
60
+ :return: Dictionary containing formatted Markdown output
61
+ """
62
+ pass
63
+
64
+ def _jira_markdown(self, data: list[T]) -> dict[str, Any]:
65
+ """
66
+ Format component summary data as Jira-flavored Markdown (not yet implemented).
67
+
68
+ This method is intended to convert component summary data into Jira-compatible
69
+ Markdown format, which may include Jira-specific syntax for tables and formatting.
70
+
71
+ :param data: List of component summary items to format
72
+ :return: Dictionary containing Jira-formatted Markdown output
73
+ """
74
+ pass
75
+
31
76
  def _get_component_summary_from_components(self, scan_components: list)-> dict:
32
77
  """
33
78
  Get a component summary from detected components.
@@ -84,10 +129,16 @@ class ComponentSummary(RawBase):
84
129
  self._get_components_data(self.results, components)
85
130
  return self._convert_components_to_list(components)
86
131
 
132
+ def _format(self, component_summary) -> str:
133
+ # TODO: Implement formatter to support dynamic outputs
134
+ json_data = self._json(component_summary)
135
+ return json.dumps(json_data, indent=2)
136
+
87
137
  def run(self):
88
138
  components = self._get_components()
89
139
  component_summary = self._get_component_summary_from_components(components)
90
- self.print_to_file_or_stdout(json.dumps(component_summary, indent=2), self.output)
140
+ output = self._format(component_summary)
141
+ self.print_to_file_or_stdout(output, self.output)
91
142
  return component_summary
92
143
  #
93
144
  # End of ComponentSummary Class
@@ -27,6 +27,7 @@ from dataclasses import dataclass
27
27
  from typing import Any, Dict, List
28
28
 
29
29
  from ..policy_check import PolicyStatus
30
+ from ..utils.markdown_utils import generate_jira_table, generate_table
30
31
  from .raw_base import RawBase
31
32
 
32
33
 
@@ -111,7 +112,7 @@ class Copyleft(RawBase[Component]):
111
112
  :param components: List of components with copyleft licenses
112
113
  :return: Dictionary with formatted Markdown details and summary
113
114
  """
114
- return self._md_summary_generator(components, self.generate_table)
115
+ return self._md_summary_generator(components, generate_table)
115
116
 
116
117
  def _jira_markdown(self, components: list[Component]) -> Dict[str, Any]:
117
118
  """
@@ -120,7 +121,7 @@ class Copyleft(RawBase[Component]):
120
121
  :param components: List of components with copyleft licenses
121
122
  :return: Dictionary with formatted Markdown details and summary
122
123
  """
123
- return self._md_summary_generator(components, self.generate_jira_table)
124
+ return self._md_summary_generator(components, generate_jira_table)
124
125
 
125
126
  def _md_summary_generator(self, components: list[Component], table_generator):
126
127
  """
@@ -23,7 +23,9 @@ SPDX-License-Identifier: MIT
23
23
  """
24
24
 
25
25
  import json
26
+ from typing import Any
26
27
 
28
+ from ..policy_check import T
27
29
  from .raw_base import RawBase
28
30
 
29
31
 
@@ -36,6 +38,46 @@ class LicenseSummary(RawBase):
36
38
  information, providing detailed summaries including copyleft analysis and license statistics.
37
39
  """
38
40
 
41
+ def _json(self, data: dict[str,Any]) -> dict[str, Any]:
42
+ """
43
+ Format license summary data as JSON.
44
+
45
+ This method is intended to return the license summary data in JSON structure
46
+ for serialization. The data should include license information with copyleft
47
+ analysis and license statistics.
48
+
49
+ :param data: List of license summary items to format
50
+ :return: Dictionary containing license summary information including:
51
+ - licenses: List of detected licenses with SPDX IDs, URLs, and copyleft status
52
+ - detectedLicenses: Total number of unique licenses
53
+ - detectedLicensesWithCopyleft: Count of licenses marked as copyleft
54
+ """
55
+ return data
56
+
57
+ def _markdown(self, data: list[T]) -> dict[str, Any]:
58
+ """
59
+ Format license summary data as Markdown (not yet implemented).
60
+
61
+ This method is intended to convert license summary data into a human-readable
62
+ Markdown format with tables and formatted sections.
63
+
64
+ :param data: List of license summary items to format
65
+ :return: Dictionary containing formatted Markdown output
66
+ """
67
+ pass
68
+
69
+ def _jira_markdown(self, data: list[T]) -> dict[str, Any]:
70
+ """
71
+ Format license summary data as Jira-flavored Markdown (not yet implemented).
72
+
73
+ This method is intended to convert license summary data into Jira-compatible
74
+ Markdown format, which may include Jira-specific syntax for tables and formatting.
75
+
76
+ :param data: List of license summary items to format
77
+ :return: Dictionary containing Jira-formatted Markdown output
78
+ """
79
+ pass
80
+
39
81
  # Define required license fields as class constants
40
82
  REQUIRED_LICENSE_FIELDS = ['spdxid', 'url', 'copyleft', 'source']
41
83
 
@@ -131,10 +173,16 @@ class LicenseSummary(RawBase):
131
173
  self._get_dependencies_data(self.results, components)
132
174
  return self._convert_components_to_list(components)
133
175
 
176
+ def _format(self, license_summary) -> str:
177
+ # TODO: Implement formatter to support dynamic outputs
178
+ json_data = self._json(license_summary)
179
+ return json.dumps(json_data, indent=2)
180
+
134
181
  def run(self):
135
182
  components = self._get_components()
136
183
  license_summary = self._get_licenses_summary_from_components(components)
137
- self.print_to_file_or_stdout(json.dumps(license_summary, indent=2), self.output)
184
+ output = self._format(license_summary)
185
+ self.print_to_file_or_stdout(output, self.output)
138
186
  return license_summary
139
187
  #
140
188
  # End of LicenseSummary Class
@@ -0,0 +1,290 @@
1
+ """
2
+ SPDX-License-Identifier: MIT
3
+
4
+ Copyright (c) 2025, 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 dataclasses import dataclass
26
+
27
+ from ...scanossbase import ScanossBase
28
+ from ...utils import scanoss_scan_results_utils
29
+ from ..utils.file_utils import load_json_file
30
+ from ..utils.markdown_utils import generate_table
31
+
32
+
33
+ @dataclass
34
+ class MatchSummaryItem:
35
+ """
36
+ Represents a single match entry in the SCANOSS results.
37
+
38
+ This data class encapsulates all the relevant information about a component
39
+ match found during scanning, including file location, license details, and
40
+ match quality metrics.
41
+ """
42
+ file: str
43
+ file_url: str
44
+ license: str
45
+ similarity: str
46
+ purl: str
47
+ purl_url: str
48
+ version: str
49
+ lines: str
50
+
51
+
52
+ @dataclass
53
+ class ComponentMatchSummary:
54
+ """
55
+ Container for categorized SCANOSS match results.
56
+
57
+ Organizes matches into two categories: full file matches and snippet matches.
58
+ This separation allows for different presentation and analysis of match types.
59
+ """
60
+ files: list[MatchSummaryItem]
61
+ snippet: list[MatchSummaryItem]
62
+
63
+ class MatchSummary(ScanossBase):
64
+ """
65
+ Generates Markdown summaries from SCANOSS scan results.
66
+
67
+ This class processes SCANOSS scan results and creates human-readable Markdown
68
+ reports with collapsible sections for file and snippet matches. The reports
69
+ include clickable links to files when a line range
70
+ prefix is provided.
71
+ """
72
+
73
+ def __init__( # noqa: PLR0913
74
+ self,
75
+ debug: bool = False,
76
+ trace: bool = False,
77
+ quiet: bool = False,
78
+ line_range_prefix: str = None,
79
+ scanoss_results_path: str = None,
80
+ output: str = None,
81
+ ):
82
+ """
83
+ Initialize the Matches Summary generator.
84
+
85
+ :param debug: Enable debug output for troubleshooting
86
+ :param trace: Enable trace-level logging for detailed execution tracking
87
+ :param quiet: Suppress informational messages
88
+ :param line_range_prefix: Base URL prefix for GitLab file links with line ranges
89
+ (e.g., 'https://gitlab.com/org/project/-/blob/main')
90
+ :param scanoss_results_path: Path to SCANOSS scan results file in JSON format
91
+ :param output: Output file path for the generated Markdown report (default: stdout)
92
+ """
93
+ super().__init__(debug=debug, trace=trace, quiet=quiet)
94
+ self.scanoss_results_path = scanoss_results_path
95
+ self.line_range_prefix = line_range_prefix
96
+ self.output = output
97
+
98
+
99
+ def _get_match_summary_item(self, file_name: str, result: dict) -> MatchSummaryItem:
100
+ """
101
+ Create a MatchSummaryItem from a single scan result.
102
+
103
+ Processes a SCANOSS scan result and creates a MatchSummaryItem with appropriate
104
+ file URLs, license information, and line ranges. Handles both snippet matches
105
+ (with specific line ranges) and file matches (entire file).
106
+
107
+ :param file_name: Name of the scanned file (relative path in the repository)
108
+ :param result: SCANOSS scan result dictionary containing match details
109
+ :return: Populated match summary item with all relevant information
110
+ """
111
+ if result.get('id') == "snippet":
112
+ # Snippet match: create URL with line range anchor
113
+ lines = scanoss_scan_results_utils.get_lines(result.get('lines'))
114
+ end_line = lines[len(lines) - 1] if len(lines) > 1 else lines[0]
115
+ file_url = f"{self.line_range_prefix}/{file_name}#L{lines[0]}-L{end_line}"
116
+ return MatchSummaryItem(
117
+ file_url=file_url,
118
+ file=file_name,
119
+ license=result.get('licenses')[0].get('name'),
120
+ similarity=result.get('matched'),
121
+ purl=result.get('purl')[0],
122
+ purl_url=result.get('url'),
123
+ version=result.get('version'),
124
+ lines=f"{lines[0]}-{lines[len(lines) - 1] if len(lines) > 1 else lines[0]}"
125
+ )
126
+ # File match: create URL without line range
127
+ return MatchSummaryItem(
128
+ file=file_name,
129
+ file_url=f"{self.line_range_prefix}/{file_name}",
130
+ license=result.get('licenses')[0].get('name'),
131
+ similarity=result.get('matched'),
132
+ purl=result.get('purl')[0],
133
+ purl_url=result.get('url'),
134
+ version=result.get('version'),
135
+ lines="all"
136
+ )
137
+
138
+ def _validate_result(self, file_name: str, result: dict) -> bool:
139
+ """
140
+ Validate that a scan result has all required fields.
141
+
142
+ :param file_name: Name of the file being validated
143
+ :param result: The scan result to validate
144
+ :return: True if valid, False otherwise
145
+ """
146
+ validations = [
147
+ ('id', 'No id found'),
148
+ ('lines', 'No lines found'),
149
+ ('purl', 'No purl found'),
150
+ ('licenses', 'No licenses found'),
151
+ ('version', 'No version found'),
152
+ ('matched', 'No matched found'),
153
+ ('url', 'No url found'),
154
+ ]
155
+
156
+ for field, error_msg in validations:
157
+ if not result.get(field):
158
+ self.print_debug(f'ERROR: {error_msg} for file {file_name}')
159
+ return False
160
+
161
+ # Additional validation for non-empty lists
162
+ if len(result.get('purl')) == 0:
163
+ self.print_debug(f'ERROR: No purl found for file {file_name}')
164
+ return False
165
+ if len(result.get('licenses')) == 0:
166
+ self.print_debug(f'ERROR: Empty licenses list for file {file_name}')
167
+ return False
168
+
169
+ return True
170
+
171
+ def _get_matches_summary(self) -> ComponentMatchSummary:
172
+ """
173
+ Parse SCANOSS scan results and create categorized match summaries.
174
+
175
+ Loads the SCANOSS scan results file and processes each match, validating
176
+ required fields and categorizing matches into file matches and snippet matches.
177
+ Skips invalid or incomplete results with debug messages.
178
+ """
179
+ # Load scan results from JSON file
180
+ scan_results = load_json_file(self.scanoss_results_path)
181
+ gitlab_matches_summary = ComponentMatchSummary(files=[], snippet=[])
182
+
183
+ # Process each file and its results
184
+ for file_name, results in scan_results.items():
185
+ for result in results:
186
+ # Skip non-matches
187
+ if result.get('id') == "none":
188
+ self.print_debug(f'Skipping non-match for file {file_name}')
189
+ continue
190
+
191
+ # Validate required fields
192
+ if not self._validate_result(file_name, result):
193
+ continue
194
+
195
+ # Create summary item and categorize by match type
196
+ summary_item = self._get_match_summary_item(file_name, result)
197
+ if result.get('id') == "snippet":
198
+ gitlab_matches_summary.snippet.append(summary_item)
199
+ else:
200
+ gitlab_matches_summary.files.append(summary_item)
201
+
202
+ return gitlab_matches_summary
203
+
204
+
205
+ def _markdown(self, gitlab_matches_summary: ComponentMatchSummary) -> str:
206
+ """
207
+ Generate Markdown from match summaries.
208
+
209
+ Creates a formatted Markdown document with collapsible sections for file
210
+ and snippet matches.
211
+
212
+ :param gitlab_matches_summary: Container with categorized file and snippet matches to format
213
+ :return: Complete Markdown document with formatted match tables
214
+ """
215
+
216
+ if len(gitlab_matches_summary.files) == 0 and len(gitlab_matches_summary.snippet) == 0:
217
+ return ""
218
+
219
+ # Define table headers
220
+ file_match_headers = ['File', 'License', 'Similarity', 'PURL', 'Version']
221
+ snippet_match_headers = ['File', 'License', 'Similarity', 'PURL', 'Version', 'Lines']
222
+ # Build file matches table
223
+ file_match_rows = []
224
+ for file_match in gitlab_matches_summary.files:
225
+ row = [
226
+ f"[{file_match.file}]({file_match.file_url})",
227
+ file_match.license,
228
+ file_match.similarity,
229
+ f"[{file_match.purl}]({file_match.purl_url})",
230
+ file_match.version,
231
+ ]
232
+ file_match_rows.append(row)
233
+ file_match_table = generate_table(file_match_headers, file_match_rows)
234
+
235
+ # Build snippet matches table
236
+ snippet_match_rows = []
237
+ for snippet_match in gitlab_matches_summary.snippet:
238
+ row = [
239
+ f"[{snippet_match.file}]({snippet_match.file_url})",
240
+ snippet_match.license,
241
+ snippet_match.similarity,
242
+ f"[{snippet_match.purl}]({snippet_match.purl_url})",
243
+ snippet_match.version,
244
+ snippet_match.lines
245
+ ]
246
+ snippet_match_rows.append(row)
247
+ snippet_match_table = generate_table(snippet_match_headers, snippet_match_rows)
248
+
249
+ # Assemble complete Markdown document
250
+ markdown = ""
251
+ markdown += "### SCANOSS Match Summary\n\n"
252
+
253
+ # File matches section (collapsible)
254
+ markdown += "<details>\n"
255
+ markdown += "<summary>File Match Summary</summary>\n\n"
256
+ markdown += file_match_table
257
+ markdown += "\n</details>\n"
258
+
259
+ # Snippet matches section (collapsible)
260
+ markdown += "<details>\n"
261
+ markdown += "<summary>Snippet Match Summary</summary>\n\n"
262
+ markdown += snippet_match_table
263
+ markdown += "\n</details>\n"
264
+
265
+ return markdown
266
+
267
+ def run(self):
268
+ """
269
+ Execute the matches summary generation process.
270
+
271
+ This is the main entry point for generating the matches summary report.
272
+ It orchestrates the entire workflow:
273
+ 1. Loads and parses SCANOSS scan results
274
+ 2. Validates and categorizes matches
275
+ 3. Generates Markdown report
276
+ 4. Outputs to file or stdout
277
+ """
278
+ # Load and process scan results into categorized matches
279
+ matches = self._get_matches_summary()
280
+
281
+ # Format matches as GitLab-compatible Markdown
282
+ matches_md = self._markdown(matches)
283
+ if matches_md == "":
284
+ self.print_stdout("No matches found.")
285
+ return
286
+ # Output to file or stdout
287
+ self.print_to_file_or_stdout(matches_md, self.output)
288
+
289
+
290
+
@@ -22,13 +22,12 @@ SPDX-License-Identifier: MIT
22
22
  THE SOFTWARE.
23
23
  """
24
24
 
25
- import json
26
- import os.path
27
25
  from abc import abstractmethod
28
26
  from enum import Enum
29
27
  from typing import Any, Dict, TypeVar
30
28
 
31
29
  from ..policy_check import PolicyCheck
30
+ from ..utils.file_utils import load_json_file
32
31
  from ..utils.license_utils import LicenseUtil
33
32
 
34
33
 
@@ -313,15 +312,11 @@ class RawBase(PolicyCheck[T]):
313
312
  Returns:
314
313
  Dict[str, Any]: The parsed JSON data
315
314
  """
316
- if not os.path.exists(self.filepath):
317
- self.print_stderr(f'ERROR: The file "{self.filepath}" does not exist.')
318
- return None
319
- with open(self.filepath, 'r') as jsonfile:
320
- try:
321
- return json.load(jsonfile)
322
- except Exception as e:
315
+ try:
316
+ return load_json_file(self.filepath)
317
+ except Exception as e:
323
318
  self.print_stderr(f'ERROR: Problem parsing input JSON: {e}')
324
- return None
319
+ return None
325
320
 
326
321
  def _convert_components_to_list(self, components: dict):
327
322
  if components is None:
@@ -27,6 +27,7 @@ from dataclasses import dataclass
27
27
  from typing import Any, Dict, List
28
28
 
29
29
  from ..policy_check import PolicyStatus
30
+ from ..utils.markdown_utils import generate_jira_table, generate_table
30
31
  from .raw_base import RawBase
31
32
 
32
33
 
@@ -193,7 +194,7 @@ class UndeclaredComponent(RawBase[Component]):
193
194
  for component in component_licenses:
194
195
  rows.append([component.get('purl'), component.get('spdxid')])
195
196
  return {
196
- 'details': f'### Undeclared components\n{self.generate_table(headers, rows)}\n',
197
+ 'details': f'### Undeclared components\n{generate_table(headers, rows)}\n',
197
198
  'summary': self._get_summary(component_licenses),
198
199
  }
199
200
 
@@ -211,7 +212,7 @@ class UndeclaredComponent(RawBase[Component]):
211
212
  for component in component_licenses:
212
213
  rows.append([component.get('purl'), component.get('spdxid')])
213
214
  return {
214
- 'details': f'{self.generate_jira_table(headers, rows)}',
215
+ 'details': f'{generate_jira_table(headers, rows)}',
215
216
  'summary': self._get_jira_summary(component_licenses),
216
217
  }
217
218
 
@@ -0,0 +1,44 @@
1
+ """
2
+ SPDX-License-Identifier: MIT
3
+
4
+ Copyright (c) 2025, 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
+ import json
26
+ import os
27
+
28
+
29
+ def load_json_file(file_path: str) -> dict:
30
+ """
31
+ Load the file
32
+
33
+ :param file_path: file path to the JSON file
34
+
35
+ Returns:
36
+ Dict[str, Any]: The parsed JSON data
37
+ """
38
+ if not os.path.exists(file_path):
39
+ raise ValueError(f'The file "{file_path}" does not exist.')
40
+ with open(file_path, 'r') as jsonfile:
41
+ try:
42
+ return json.load(jsonfile)
43
+ except Exception as e:
44
+ raise ValueError(f'ERROR: Problem parsing input JSON: {e}')
@@ -0,0 +1,63 @@
1
+ """
2
+ SPDX-License-Identifier: MIT
3
+
4
+ Copyright (c) 2025, 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
+ def generate_table(headers, rows, centered_columns=None):
26
+ """
27
+ Generate a Markdown table.
28
+
29
+ :param headers: List of headers for the table.
30
+ :param rows: List of rows for the table.
31
+ :param centered_columns: List of column indices to be centered.
32
+ :return: A string representing the Markdown table.
33
+ """
34
+ col_sep = ' | '
35
+ centered_column_set = set(centered_columns or [])
36
+ if headers is None:
37
+ return None
38
+
39
+ # Decide which separator to use
40
+ def create_separator(index):
41
+ if centered_columns is None:
42
+ return '-'
43
+ return ':-:' if index in centered_column_set else '-'
44
+
45
+ # Build the row separator
46
+ row_separator = col_sep + col_sep.join(create_separator(index) for index, _ in enumerate(headers)) + col_sep
47
+ # build table rows
48
+ table_rows = [col_sep + col_sep.join(headers) + col_sep, row_separator]
49
+ table_rows.extend(col_sep + col_sep.join(row) + col_sep for row in rows)
50
+ return '\n'.join(table_rows)
51
+
52
+ def generate_jira_table(headers, rows, centered_columns=None):
53
+ col_sep = '*|*'
54
+ if headers is None:
55
+ return None
56
+
57
+ table_header = '|*' + col_sep.join(headers) + '*|\n'
58
+ table = table_header
59
+ for row in rows:
60
+ if len(headers) == len(row):
61
+ table += '|' + '|'.join(row) + '|\n'
62
+
63
+ return table
@@ -158,6 +158,7 @@ class FolderHasher:
158
158
  filtered_files.sort()
159
159
 
160
160
  bar = Bar('Hashing files...', max=len(filtered_files))
161
+ full_file_path = ''
161
162
  for file_path in filtered_files:
162
163
  try:
163
164
  file_path_obj = Path(file_path) if isinstance(file_path, str) else file_path
@@ -0,0 +1,41 @@
1
+ """
2
+ SPDX-License-Identifier: MIT
3
+
4
+ Copyright (c) 2025, 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
+ def get_lines(lines: str) -> list:
26
+ """
27
+ Parse line range string into a list of line numbers.
28
+
29
+ Converts SCANOSS line notation (e.g., '10-20,25-30') into a flat list
30
+ of individual line numbers for processing.
31
+
32
+ :param lines: Comma-separated line ranges in SCANOSS format (e.g., '10-20,25-30')
33
+ :return: Flat list of all line numbers extracted from the ranges
34
+ """
35
+ lines_list = []
36
+ lines = lines.split(',')
37
+ for line in lines:
38
+ line_parts = line.split('-')
39
+ for part in line_parts:
40
+ lines_list.append(int(part))
41
+ return lines_list
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: scanoss
3
- Version: 1.38.0
3
+ Version: 1.39.0
4
4
  Summary: Simple Python library to leverage the SCANOSS APIs
5
5
  Home-page: https://scanoss.com
6
6
  Author: SCANOSS
@@ -6,8 +6,8 @@ protoc_gen_swagger/options/annotations_pb2_grpc.py,sha256=KZOW9Ciio-f9iL42FuLFnS
6
6
  protoc_gen_swagger/options/openapiv2_pb2.py,sha256=w0xDs63uyrWGgzRaQZXfJpfI7Jpyvh-i9ay_uzOR-aM,16475
7
7
  protoc_gen_swagger/options/openapiv2_pb2.pyi,sha256=hYOV6uQ2yqhP89042_V3GuAsvoBBiXf5CGuYmnFnfv4,54665
8
8
  protoc_gen_swagger/options/openapiv2_pb2_grpc.py,sha256=sje9Nh3yE7CHCUWZwtjTgwsKB4GvyGz5vOrGTnRXJfc,917
9
- scanoss/__init__.py,sha256=XYJ-FpfDnKK7tuc0ms8xzYcxUDkA9f2cQOSi2LvWfTA,1146
10
- scanoss/cli.py,sha256=1d6toykJ_rNY8XInFmzwXR2m6oB70GHO7kpb-uM-sm8,97834
9
+ scanoss/__init__.py,sha256=q6qrK1ovnkPF_j1UjeGsMzCShu2ZJ4lwVjxJWcnkEro,1146
10
+ scanoss/cli.py,sha256=u8t-F5TubEY7XnMtOdpQyf8hHQTWgJV6NLSAbO70KZY,103272
11
11
  scanoss/components.py,sha256=NFyt_w3aoMotr_ZaFU-ng00_89sruc0kgY7ERnJXkmM,15891
12
12
  scanoss/constants.py,sha256=GHLTaLNVxXdTXRj7ngRK4u4S653pHzM8qFy4JFLa0wQ,450
13
13
  scanoss/cryptography.py,sha256=lOoD_dW16ARQxYiYyb5R8S7gx0FqWIsnGkKfsB0nGaU,10627
@@ -16,6 +16,7 @@ scanoss/cyclonedx.py,sha256=mHeX66yQCk41N3YCIzKy_fI7fLqQnetYPFRIzUKy_M4,18416
16
16
  scanoss/delta.py,sha256=slmgnD7SsUOmfSE2zb0zdRAGo-JcjPJAtxyzuCSzO3I,9455
17
17
  scanoss/file_filters.py,sha256=QcLqunaBKQIafjNZ9_Snh9quBX5_-fsTusVmxwjC1q8,18511
18
18
  scanoss/filecount.py,sha256=RZjKQ6M5P_RQg0_PMD2tsRe5Z8f98ke0sxYVjPDN8iQ,6538
19
+ scanoss/gitlabqualityreport.py,sha256=JNDT63gxTOxLONe4dxUzn1o7eVniK0B3j6SiFTRLinw,6790
19
20
  scanoss/results.py,sha256=47ZXXuU2sDjYa5vhtbWTmikit9jHhA0rsYKwkvZFI5w,9252
20
21
  scanoss/scancodedeps.py,sha256=JbpoGW1POtPMmowzfwa4oh8sSBeeQCqaW9onvc4UFYM,11517
21
22
  scanoss/scanner.py,sha256=-RCxLX0EepUebK8jQKvlMxFEQrCc8SwEjxznoWjadkg,45510
@@ -64,24 +65,28 @@ scanoss/api/vulnerabilities/__init__.py,sha256=IFrDk_DTJgKSZmmU-nuLXuq_s8sQZlrSC
64
65
  scanoss/api/vulnerabilities/v2/__init__.py,sha256=IFrDk_DTJgKSZmmU-nuLXuq_s8sQZlrSCHhIDMJT4r0,1122
65
66
  scanoss/api/vulnerabilities/v2/scanoss_vulnerabilities_pb2.py,sha256=pmm0MSiXkdf8e4rCIIDRcsNRixR2vGvD1Xak4l-wdwI,16550
66
67
  scanoss/api/vulnerabilities/v2/scanoss_vulnerabilities_pb2_grpc.py,sha256=BNxT5kUKQ-mgtOt5QYBM1Qrg5LNDqSpWKpfEZquIlsM,19127
67
- scanoss/data/build_date.txt,sha256=vIDp8v5oIkuI4Pxunu5cDrnq1xF9Ajjgdjwun5NfN74,40
68
+ scanoss/data/build_date.txt,sha256=E98VubnEloGNCDN8aqw3fIG_fDuiNX2rH1wNr9RiqVk,40
68
69
  scanoss/data/scanoss-settings-schema.json,sha256=ClkRYAkjAN0Sk704G8BE_Ok006oQ6YnIGmX84CF8h9w,8798
69
70
  scanoss/data/spdx-exceptions.json,sha256=s7UTYxC7jqQXr11YBlIWYCNwN6lRDFTR33Y8rpN_dA4,17953
70
71
  scanoss/data/spdx-licenses.json,sha256=A6Z0q82gaTLtnopBfzeIVZjJFxkdRW1g2TuumQc-lII,228794
71
72
  scanoss/export/__init__.py,sha256=D4C0lWLuNp8k_BjQZEc07WZcUgAvriVwQWOk063b0ZU,1122
72
73
  scanoss/export/dependency_track.py,sha256=A_xQH6_r9xL_fth1Wr770GCTRFVyn7XcUPfVUsXp4-w,9271
73
74
  scanoss/inspection/__init__.py,sha256=D4C0lWLuNp8k_BjQZEc07WZcUgAvriVwQWOk063b0ZU,1122
74
- scanoss/inspection/policy_check.py,sha256=JOJko_QVB7_6I8VQiGFJmOmJheN5jlwtpGOS2kBMMCo,9756
75
- scanoss/inspection/dependency_track/project_violation.py,sha256=zmTxxUQe2xuq-mHq1audsvFF3co0BoHy-EuSpNErw7U,20593
76
- scanoss/inspection/raw/component_summary.py,sha256=J4DDGNg9WIxIaTeblk6u4tmtMf4veXDesuC4rmpHNkM,4090
77
- scanoss/inspection/raw/copyleft.py,sha256=xAKIYROUG-F9SbPs3iIDmTg8yqovh3NVZNni4-byd68,9324
78
- scanoss/inspection/raw/license_summary.py,sha256=m5JVcqnqViPLrwHI5-XCpIEQzUiKtnhFC3FWhfM0T00,5823
79
- scanoss/inspection/raw/raw_base.py,sha256=gQoInr8a82JxzGjtdBo61ffVJ8sYJ9HNcTNzze1JXKI,18346
80
- scanoss/inspection/raw/undeclared_component.py,sha256=uN-oVqQF8vWArTc2yDVoxudV0bbrkbZaZ-mdZn-Cntw,11361
75
+ scanoss/inspection/policy_check.py,sha256=DtDZKeNe_WltF42BoiMbns4v8m7XhHVziqIc3RWzDEA,8171
76
+ scanoss/inspection/dependency_track/project_violation.py,sha256=x9PVk9mh5BU9Qr9WiDiRvwTTZxPzYFjzd11miZP-lpM,20654
77
+ scanoss/inspection/raw/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
78
+ scanoss/inspection/raw/component_summary.py,sha256=axuzDxMdWva4HAM_i9D-NpqzPZKRd2Jp3oKq9R1oE88,6407
79
+ scanoss/inspection/raw/copyleft.py,sha256=ZOJ6FOvg2kx2pYOOCVza301h9c3ZB_jGNp-Lp4TTFcY,9385
80
+ scanoss/inspection/raw/license_summary.py,sha256=kW0mHztuAbOXI7G1-7ajzq7A8qhgSqANi0W6n-jOF5Y,7786
81
+ scanoss/inspection/raw/match_summary.py,sha256=3Mhp-H7OmMxhvSxR_opfl-xHB4UfDfTM-io5HsEH4co,11386
82
+ scanoss/inspection/raw/raw_base.py,sha256=mzdBs2dAaZlmYW9U8PnkvnjWnWyXWMkHzyhZ9FCM5Hc,18166
83
+ scanoss/inspection/raw/undeclared_component.py,sha256=mnSJr7RjUs2YT46xiBNkwx3Jrulo_8QtuagWAFL5KLo,11422
84
+ scanoss/inspection/utils/file_utils.py,sha256=b-xTH6FSyPpl3EPZ9WzK0c4734yE9mAexT1_YLLqymE,1641
81
85
  scanoss/inspection/utils/license_utils.py,sha256=Zb6QLmVJb86lKCwZyBsmwakyAtY1SXa54kUyyKmWMqA,5093
86
+ scanoss/inspection/utils/markdown_utils.py,sha256=zkFs48TM-NR6nUHYOyQmHCwV82_fUsks5UB4BmyGifU,2446
82
87
  scanoss/scanners/__init__.py,sha256=D4C0lWLuNp8k_BjQZEc07WZcUgAvriVwQWOk063b0ZU,1122
83
88
  scanoss/scanners/container_scanner.py,sha256=fOrb64owrstX7LnTuxiIan059YgLeKXeBS6g2QaCyq0,16346
84
- scanoss/scanners/folder_hasher.py,sha256=tOlo8YXanC3sQ77oHvHcsWiu8BW2pCfTyWC07AmzHIQ,12845
89
+ scanoss/scanners/folder_hasher.py,sha256=PD1tghOrra3KtfsZJUbqKOmIBF-0Tg14FcBCKkqGUis,12873
85
90
  scanoss/scanners/scanner_config.py,sha256=egG7cw3S2akU-D9M1aLE5jLrfz_c8e7_DIotMnnpM84,2601
86
91
  scanoss/scanners/scanner_hfh.py,sha256=M2PB4wDTi4LD1DwuAVfWiqQkjOImSpNok7vgo5H_Spg,9190
87
92
  scanoss/services/dependency_track_service.py,sha256=JIpqev4I-x_ZajMxD5W2Y3OAUvEJ_4nstzAPV90vfP8,5070
@@ -89,10 +94,11 @@ scanoss/utils/__init__.py,sha256=0hjb5ktavp7utJzFhGMPImPaZiHWgilM2HwvTp5lXJE,112
89
94
  scanoss/utils/abstract_presenter.py,sha256=teiDTxBj5jBMCk2T8i4l1BJPf_u4zBLWrtCTFHSSECM,3148
90
95
  scanoss/utils/crc64.py,sha256=TMrwQimSdE6imhFOUL7oAG6Kxu-8qMpGWMuMg8QpSVs,3169
91
96
  scanoss/utils/file.py,sha256=62cA9a17TU9ZvfA3FY5HY4-QOajJeSrc8S6xLA_f-3M,2980
97
+ scanoss/utils/scanoss_scan_results_utils.py,sha256=ho9-DKefHFJlVZkw4gXOmMI-mgPIbV9Y2ftkI83fC1k,1727
92
98
  scanoss/utils/simhash.py,sha256=6iu8DOcecPAY36SZjCOzrrLMT9oIE7-gI6QuYwUQ7B0,5793
93
- scanoss-1.38.0.dist-info/licenses/LICENSE,sha256=LLUaXoiyOroIbr5ubAyrxBOwSRLTm35ETO2FmLpy8QQ,1074
94
- scanoss-1.38.0.dist-info/METADATA,sha256=nGYHuWaHvC-qnnBCwnLwVt3-jfPV1W3Riu0W_2QVraI,6181
95
- scanoss-1.38.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
96
- scanoss-1.38.0.dist-info/entry_points.txt,sha256=Uy28xnaDL5KQ7V77sZD5VLDXPNxYYzSr5tsqtiXVzAs,48
97
- scanoss-1.38.0.dist-info/top_level.txt,sha256=V11PrQ6Pnrc-nDF9xnisnJ8e6-i7HqSIKVNqduRWcL8,27
98
- scanoss-1.38.0.dist-info/RECORD,,
99
+ scanoss-1.39.0.dist-info/licenses/LICENSE,sha256=LLUaXoiyOroIbr5ubAyrxBOwSRLTm35ETO2FmLpy8QQ,1074
100
+ scanoss-1.39.0.dist-info/METADATA,sha256=SfWyYWos1ELzQhh1fOPEPCrv2ZzlyEIRUrQ-k26Qg1k,6181
101
+ scanoss-1.39.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
102
+ scanoss-1.39.0.dist-info/entry_points.txt,sha256=Uy28xnaDL5KQ7V77sZD5VLDXPNxYYzSr5tsqtiXVzAs,48
103
+ scanoss-1.39.0.dist-info/top_level.txt,sha256=V11PrQ6Pnrc-nDF9xnisnJ8e6-i7HqSIKVNqduRWcL8,27
104
+ scanoss-1.39.0.dist-info/RECORD,,