Skip to content

Commit 748a91c

Browse files
committed
feat: SP-3574 add GitLab matches summary report generator into inspect command
Implements GitLab-compatible Markdown summary reports for SCANOSS matches with the following changes: - Add new 'inspect gitlab matches' CLI command to generate match summaries - Create MatchSummary class to process and format scan results with collapsible sections - Extract table generation utilities into shared markdown_utils module - Extract JSON file loading into shared file_utils module - Add support for GitLab file links with line range anchors
1 parent d930165 commit 748a91c

File tree

12 files changed

+468
-63
lines changed

12 files changed

+468
-63
lines changed

CHANGELOG.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1111

1212
## [1.38.0] - 2025-10-22
1313
### Added
14-
- Added GitLab Code Quality report format support
14+
- Added `glc-codequality` format to convert subcomand
15+
- Added `inspect gitlab matches` subcommand to generate GitLab-compatible Markdown match summary from SCANOSS scan results
16+
- Added utility modules for shared functionality (`markdown_utils.py` and `file_utils.py`)
17+
### Changed
18+
- Refactored table generation utilities into shared `markdown_utils` module
19+
- Refactored JSON file loading into shared `file_utils` module
1520

1621
## [1.37.1] - 2025-10-21
1722
### Added

src/scanoss/cli.py

Lines changed: 118 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
)
4141
from scanoss.inspection.raw.component_summary import ComponentSummary
4242
from scanoss.inspection.raw.license_summary import LicenseSummary
43+
from scanoss.inspection.raw.match_summary import MatchSummary
4344
from scanoss.scanners.container_scanner import (
4445
DEFAULT_SYFT_COMMAND,
4546
DEFAULT_SYFT_TIMEOUT,
@@ -73,6 +74,7 @@
7374
from .csvoutput import CsvOutput
7475
from .cyclonedx import CycloneDx
7576
from .filecount import FileCount
77+
from .gitlabqualityreport import GitLabQualityReport
7678
from .inspection.raw.copyleft import Copyleft
7779
from .inspection.raw.undeclared_component import UndeclaredComponent
7880
from .results import Results
@@ -85,7 +87,6 @@
8587
from .spdxlite import SpdxLite
8688
from .threadeddependencies import SCOPE
8789
from .utils.file import validate_json_file
88-
from .gitlabqualityreport import GitLabQualityReport
8990

9091
HEADER_PARTS_COUNT = 2
9192

@@ -284,7 +285,7 @@ def setup_args() -> None: # noqa: PLR0912, PLR0915
284285
'--format',
285286
'-f',
286287
type=str,
287-
choices=['cyclonedx', 'spdxlite', 'csv', 'glcodequality'],
288+
choices=['cyclonedx', 'spdxlite', 'csv', 'glc-codequality'],
288289
default='spdxlite',
289290
help='Output format (optional - default: spdxlite)',
290291
)
@@ -795,6 +796,64 @@ def setup_args() -> None: # noqa: PLR0912, PLR0915
795796
help='Timeout (in seconds) for API communication (optional - default 300 sec)',
796797
)
797798

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+
nargs='?',
836+
help='Path to SCANOSS scan results file (JSON format) to analyze'
837+
)
838+
839+
# Line range prefix for GitLab file navigation
840+
# Enables clickable file references in the generated report that link to specific lines in GitLab
841+
p_gl_inspect_matches.add_argument(
842+
'-lpr',
843+
'--line-range-prefix',
844+
nargs='?',
845+
help='Base URL prefix for GitLab file links with line ranges (e.g., https://gitlab.com/org/project/-/blob/main)'
846+
)
847+
848+
# Output file argument - where to save the generated Markdown report
849+
p_gl_inspect_matches.add_argument(
850+
'--output',
851+
'-o',
852+
required=False,
853+
type=str,
854+
help='Output file path for the generated Markdown report (default: stdout)'
855+
)
856+
798857
# TODO Move to the command call def location
799858
# RAW results
800859
p_inspect_raw_undeclared.set_defaults(func=inspect_undeclared)
@@ -809,6 +868,9 @@ def setup_args() -> None: # noqa: PLR0912, PLR0915
809868
# Dependency Track
810869
p_inspect_dt_project_violation.set_defaults(func=inspect_dep_track_project_violations)
811870

871+
# GitLab
872+
p_gl_inspect_matches.set_defaults(func=inspect_gitlab_matches)
873+
812874
# =========================================================================
813875
# END INSPECT SUBCOMMAND CONFIGURATION
814876
# =========================================================================
@@ -1157,6 +1219,7 @@ def setup_args() -> None: # noqa: PLR0912, PLR0915
11571219
p_inspect_legacy_license_summary,
11581220
p_inspect_legacy_component_summary,
11591221
p_inspect_dt_project_violation,
1222+
p_gl_inspect_matches,
11601223
c_provenance,
11611224
p_folder_scan,
11621225
p_folder_hash,
@@ -1613,7 +1676,7 @@ def convert(parser, args):
16131676
print_stderr('Producing CSV report...')
16141677
csvo = CsvOutput(debug=args.debug, output_file=args.output)
16151678
success = csvo.produce_from_file(args.input)
1616-
elif args.format == 'glcodequality':
1679+
elif args.format == 'glc-codequality':
16171680
if not args.quiet:
16181681
print_stderr('Producing Gitlab code quality report...')
16191682
glcCodeQuality = GitLabQualityReport(debug=args.debug, output_file=args.output)
@@ -1891,6 +1954,58 @@ def inspect_dep_track_project_violations(parser, args):
18911954
sys.exit(1)
18921955

18931956

1957+
def inspect_gitlab_matches(parser, args):
1958+
"""
1959+
Handle GitLab matches summary inspection command.
1960+
1961+
Analyzes SCANOSS scan results and generates a GitLab-compatible Markdown summary
1962+
report of component matches. The report includes match details, file locations,
1963+
and optionally clickable links to source files in GitLab repositories.
1964+
1965+
This command processes SCANOSS scan output and creates human-readable Markdown.
1966+
1967+
Parameters
1968+
----------
1969+
args : Namespace
1970+
Parsed command line arguments containing:
1971+
- input: Path to SCANOSS scan results file (JSON format) to analyze
1972+
- line_range_prefix: Base URL prefix for generating GitLab file links with line ranges
1973+
(e.g., 'https://gitlab.com/org/project/-/blob/main')
1974+
- output: Optional output file path for the generated Markdown report (default: stdout)
1975+
- debug: Enable debug output for troubleshooting
1976+
- trace: Enable trace-level logging
1977+
- quiet: Suppress informational messages
1978+
1979+
Notes
1980+
-----
1981+
- The output is formatted in Markdown for optimal display in GitLab
1982+
- Line range prefix enables clickable file references in the report
1983+
- If output is not specified, the report is written to stdout
1984+
"""
1985+
# Initialize output file if specified (create/truncate)
1986+
if args.output:
1987+
initialise_empty_file(args.output)
1988+
1989+
try:
1990+
# Create GitLab matches summary generator with configuration
1991+
match_summary = MatchSummary(
1992+
debug=args.debug,
1993+
trace=args.trace,
1994+
quiet=args.quiet,
1995+
scanoss_results_path=args.input, # Path to SCANOSS JSON results
1996+
output=args.output, # Output file path or None for stdout
1997+
line_range_prefix=args.line_range_prefix, # GitLab URL prefix for file links
1998+
)
1999+
2000+
# Execute the summary generation
2001+
match_summary.run()
2002+
except Exception as e:
2003+
# Handle any errors during report generation
2004+
print_stderr(e)
2005+
if args.debug:
2006+
traceback.print_exc()
2007+
sys.exit(1)
2008+
18942009
# =============================================================================
18952010
# END INSPECT COMMAND HANDLERS
18962011
# =============================================================================

src/scanoss/gitlabqualityreport.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
from .scanossbase import ScanossBase
77

8+
89
@dataclass
910
class Lines:
1011
begin: int
@@ -73,8 +74,10 @@ def _get_code_quality(self, file_name: str,result: dict) -> CodeQuality or None:
7374
if len(lines) == 0:
7475
self.print_debug(f"Warning: empty lines for result: {result}")
7576
return None
77+
end_line = lines[len(lines) - 1] if len(lines) > 1 else lines[0]
78+
description = f"Snippet found in: {file_name} - lines {lines[0]}-{end_line}"
7679
return CodeQuality(
77-
description=f"Snippet found in: {file_name} - lines {lines[0]}-{lines[len(lines) - 1] if len(lines) > 1 else lines[0]}",
80+
description=description,
7881
check_name=file_name,
7982
fingerprint=result.get('file_hash'),
8083
severity="info",

src/scanoss/inspection/dependency_track/project_violation.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828

2929
from ...services.dependency_track_service import DependencyTrackService
3030
from ..policy_check import PolicyCheck, PolicyStatus
31+
from ..utils.markdown_utils import generate_jira_table, generate_table
3132

3233
# Constants
3334
PROCESSING_RETRY_DELAY = 5 # seconds
@@ -195,7 +196,7 @@ def _markdown(self, project_violations: list[PolicyViolationDict]) -> Dict[str,
195196
Returns:
196197
Dictionary with formatted Markdown details and summary
197198
"""
198-
return self._md_summary_generator(project_violations, self.generate_table)
199+
return self._md_summary_generator(project_violations, generate_table)
199200

200201
def _jira_markdown(self, data: list[PolicyViolationDict]) -> Dict[str, Any]:
201202
"""
@@ -207,7 +208,7 @@ def _jira_markdown(self, data: list[PolicyViolationDict]) -> Dict[str, Any]:
207208
Returns:
208209
Dictionary containing Jira markdown formatted results and summary
209210
"""
210-
return self._md_summary_generator(data, self.generate_jira_table)
211+
return self._md_summary_generator(data, generate_jira_table)
211212

212213
def is_project_updated(self, dt_project: Dict[str, Any]) -> bool:
213214
"""

src/scanoss/inspection/policy_check.py

Lines changed: 0 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -137,48 +137,6 @@ def _jira_markdown(self, data: list[T]) -> Dict[str, Any]:
137137
"""
138138
pass
139139

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-
182140
def _get_formatter(self) -> Callable[[List[dict]], Dict[str, Any]] or None:
183141
"""
184142
Get the appropriate formatter function based on the specified format.

src/scanoss/inspection/raw/__init__.py

Whitespace-only changes.

src/scanoss/inspection/raw/copyleft.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
from typing import Any, Dict, List
2828

2929
from ..policy_check import PolicyStatus
30+
from ..utils.markdown_utils import generate_jira_table, generate_table
3031
from .raw_base import RawBase
3132

3233

@@ -111,7 +112,7 @@ def _markdown(self, components: list[Component]) -> Dict[str, Any]:
111112
:param components: List of components with copyleft licenses
112113
:return: Dictionary with formatted Markdown details and summary
113114
"""
114-
return self._md_summary_generator(components, self.generate_table)
115+
return self._md_summary_generator(components, generate_table)
115116

116117
def _jira_markdown(self, components: list[Component]) -> Dict[str, Any]:
117118
"""
@@ -120,7 +121,7 @@ def _jira_markdown(self, components: list[Component]) -> Dict[str, Any]:
120121
:param components: List of components with copyleft licenses
121122
:return: Dictionary with formatted Markdown details and summary
122123
"""
123-
return self._md_summary_generator(components, self.generate_jira_table)
124+
return self._md_summary_generator(components, generate_jira_table)
124125

125126
def _md_summary_generator(self, components: list[Component], table_generator):
126127
"""

0 commit comments

Comments
 (0)