Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added
- Upcoming changes...

## [1.40.1] - 2025-10-29
### Changed
- Refactored inspect module structure for better organization
- Reorganized inspection modules into `policy_check` and `summary` subdirectories
- Moved copyleft and undeclared component checks to `policy_check/scanoss/`
- Moved component, license, and match summaries to `summary/`
- Moved Dependency Track policy checks to `policy_check/dependency_track/`
- Extracted common scan result processing logic into `ScanResultProcessor` utility class
- Improved type safety with `PolicyOutput` named tuple for policy check results
- Made `PolicyCheck` class explicitly abstract with ABC
### Added
- Added Makefile targets for running ruff linter (`linter`, `linter-fix`, `linter-docker`, `linter-docker-fix`)

## [1.40.0] - 2025-10-29
### Added
- Add support for `--rest` to `folder-scan` command
Expand Down Expand Up @@ -716,3 +729,4 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
[1.38.0]: https://github.com/scanoss/scanoss.py/compare/v1.37.1...v1.38.0
[1.39.0]: https://github.com/scanoss/scanoss.py/compare/v1.38.0...v1.39.0
[1.40.0]: https://github.com/scanoss/scanoss.py/compare/v1.39.0...v1.40.0
[1.40.1]: https://github.com/scanoss/scanoss.py/compare/v1.40.0...v1.40.1
15 changes: 15 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@ dev_setup: date_time_clean ## Setup Python dev env for the current user
@echo "Setting up dev env for the current user..."
pip3 install -e .

dev_install: ## Install dev dependencies
pip3 install -r requirements-dev.txt

dev_uninstall: ## Uninstall Python dev setup for the current user
@echo "Uninstalling dev env..."
pip3 uninstall -y scanoss
Expand All @@ -50,6 +53,18 @@ publish_test: ## Publish the Python package to TestPyPI
@echo "Publishing package to TestPyPI..."
twine upload --repository testpypi dist/*

lint-docker: ## Run ruff linter with docker
@./tools/linter.sh --docker

lint-docker-fix: ## Run ruff linter with docker and auto-fix
@./tools/linter.sh --docker --fix

lint: ## Run ruff linter locally
@./tools/linter.sh

lint-fix: ## Run ruff linter locally with auto-fix
@./tools/linter.sh --fix
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we use ruff's pre-commit hook we wouldn't need all these make commands


publish: ## Publish Python package to PyPI
@echo "Publishing package to PyPI..."
twine upload dist/*
Expand Down
2 changes: 1 addition & 1 deletion src/scanoss/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,4 @@
THE SOFTWARE.
"""

__version__ = '1.40.0'
__version__ = '1.40.1'
17 changes: 8 additions & 9 deletions src/scanoss/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,12 +35,6 @@
from scanoss.cryptography import Cryptography, create_cryptography_config_from_args
from scanoss.delta import Delta
from scanoss.export.dependency_track import DependencyTrackExporter
from scanoss.inspection.dependency_track.project_violation import (
DependencyTrackProjectViolationPolicyCheck,
)
from scanoss.inspection.raw.component_summary import ComponentSummary
from scanoss.inspection.raw.license_summary import LicenseSummary
from scanoss.inspection.raw.match_summary import MatchSummary
from scanoss.scanners.container_scanner import (
DEFAULT_SYFT_COMMAND,
DEFAULT_SYFT_TIMEOUT,
Expand Down Expand Up @@ -75,8 +69,14 @@
from .cyclonedx import CycloneDx
from .filecount import FileCount
from .gitlabqualityreport import GitLabQualityReport
from .inspection.raw.copyleft import Copyleft
from .inspection.raw.undeclared_component import UndeclaredComponent
from .inspection.policy_check.dependency_track.project_violation import (
DependencyTrackProjectViolationPolicyCheck,
)
from .inspection.policy_check.scanoss.copyleft import Copyleft
from .inspection.policy_check.scanoss.undeclared_component import UndeclaredComponent
from .inspection.summary.component_summary import ComponentSummary
from .inspection.summary.license_summary import LicenseSummary
from .inspection.summary.match_summary import MatchSummary
from .results import Results
from .scancodedeps import ScancodeDeps
from .scanner import FAST_WINNOWING, Scanner
Expand Down Expand Up @@ -1753,7 +1753,6 @@ def inspect_copyleft(parser, args):
exclude=args.exclude, # Licenses to ignore
explicit=args.explicit, # Explicit license list
)

# Execute inspection and exit with appropriate status code
status, _ = i_copyleft.run()
sys.exit(status)
Expand Down
37 changes: 33 additions & 4 deletions src/scanoss/gitlabqualityreport.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,16 +74,21 @@ def __init__(self, debug: bool = False, trace: bool = False, quiet: bool = False
Initialise the GitLabCodeQuality class
"""
super().__init__(debug, trace, quiet)
self.print_trace(f"GitLabQualityReport initialized with debug={debug}, trace={trace}, quiet={quiet}")


def _get_code_quality(self, file_name: str, result: dict) -> CodeQuality or None:
self.print_trace(f"_get_code_quality called for file: {file_name}")
self.print_trace(f"Processing result: {result}")

if not result.get('file_hash'):
self.print_debug(f"Warning: no hash found for result: {result}")
return None

if result.get('id') == 'file':
self.print_debug(f"Processing file match for: {file_name}")
description = f"File match found in: {file_name}"
return CodeQuality(
code_quality = CodeQuality(
description=description,
check_name=file_name,
fingerprint=result.get('file_hash'),
Expand All @@ -95,17 +100,21 @@ def _get_code_quality(self, file_name: str, result: dict) -> CodeQuality or None
)
)
)
self.print_trace(f"Created file CodeQuality object: {code_quality}")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need this to be traced?

return code_quality

if not result.get('lines'):
self.print_debug(f"Warning: No lines found for result: {result}")
return None
lines = scanoss_scan_results_utils.get_lines(result.get('lines'))
self.print_trace(f"Extracted lines: {lines}")
if len(lines) == 0:
self.print_debug(f"Warning: empty lines for result: {result}")
return None
end_line = lines[len(lines) - 1] if len(lines) > 1 else lines[0]
description = f"Snippet found in: {file_name} - lines {lines[0]}-{end_line}"
return CodeQuality(
self.print_debug(f"Processing snippet match for: {file_name}, lines: {lines[0]}-{end_line}")
code_quality = CodeQuality(
description=description,
check_name=file_name,
fingerprint=result.get('file_hash'),
Expand All @@ -117,35 +126,47 @@ def _get_code_quality(self, file_name: str, result: dict) -> CodeQuality or None
)
)
)
self.print_trace(f"Created snippet CodeQuality object: {code_quality}")
return code_quality

def _write_output(self, data: list[CodeQuality], output_file: str = None) -> bool:
"""Write the Gitlab Code Quality Report to output."""
self.print_trace(f"_write_output called with {len(data)} items, output_file: {output_file}")
try:
json_data = [item.to_dict() for item in data]
self.print_trace(f"JSON data: {json_data}")
file = open(output_file, 'w') if output_file else sys.stdout
print(json.dumps(json_data, indent=2), file=file)
if output_file:
file.close()
self.print_debug(f"Wrote output to file: {output_file}")
else:
self.print_debug("Wrote output to 'stdout'")
return True
except Exception as e:
self.print_stderr(f'Error writing output: {str(e)}')
return False

def _produce_from_json(self, data: dict, output_file: str = None) -> bool:
self.print_trace(f"_produce_from_json called with output_file: {output_file}")
self.print_debug(f"Processing {len(data)} files from JSON data")
code_quality = []
for file_name, results in data.items():
self.print_trace(f"Processing file: {file_name} with {len(results)} results")
for result in results:
if not result.get('id'):
self.print_debug(f"Warning: No ID found for result: {result}")
continue
if result.get('id') != 'snippet' and result.get('id') != 'file':
self.print_debug(f"Skipping non-snippet/file match: {result}")
self.print_debug(f"Skipping non-snippet/file match: {file_name}, id: '{result['id']}'")
continue
code_quality_item = self._get_code_quality(file_name, result)
if code_quality_item:
code_quality.append(code_quality_item)
self.print_trace(f"Added code quality item for {file_name}")
else:
self.print_debug(f"Warning: No Code Quality found for result: {result}")
self.print_debug(f"Generated {len(code_quality)} code quality items")
self._write_output(data=code_quality,output_file=output_file)
return True

Expand All @@ -156,11 +177,15 @@ def _produce_from_str(self, json_str: str, output_file: str = None) -> bool:
:param output_file: Output file (optional)
:return: True if successful, False otherwise
"""
self.print_trace(f"_produce_from_str called with output_file: {output_file}")
if not json_str:
self.print_stderr('ERROR: No JSON string provided to parse.')
return False
self.print_debug(f"Parsing JSON string of length: {len(json_str)}")
try:
data = json.loads(json_str)
self.print_debug("Successfully parsed JSON data")
self.print_trace(f"Parsed data structure: {type(data)}")
except Exception as e:
self.print_stderr(f'ERROR: Problem parsing input JSON: {e}')
return False
Expand All @@ -174,12 +199,16 @@ def produce_from_file(self, json_file: str, output_file: str = None) -> bool:
:param output_file:
:return: True if successful, False otherwise
"""
self.print_trace(f"produce_from_file called with json_file: {json_file}, output_file: {output_file}")
self.print_debug(f"Input JSON file: {json_file}, output_file: {output_file}")
if not json_file:
self.print_stderr('ERROR: No JSON file provided to parse.')
return False
if not os.path.isfile(json_file):
self.print_stderr(f'ERROR: JSON file does not exist or is not a file: {json_file}')
return False
self.print_debug(f"Reading JSON file: {json_file}")
with open(json_file, 'r') as f:
success = self._produce_from_str(f.read(), output_file)
json_content = f.read()
success = self._produce_from_str(json_content, output_file)
return success
Empty file.
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,9 @@
from datetime import datetime
from typing import Any, Dict, List, Optional, TypedDict

from ...services.dependency_track_service import DependencyTrackService
from ..policy_check import PolicyCheck, PolicyStatus
from ..utils.markdown_utils import generate_jira_table, generate_table
from ....services.dependency_track_service import DependencyTrackService
from ...utils.markdown_utils import generate_jira_table, generate_table
from ..policy_check import PolicyCheck, PolicyOutput, PolicyStatus

# Constants
PROCESSING_RETRY_DELAY = 5 # seconds
Expand Down Expand Up @@ -171,7 +171,7 @@ def __init__( # noqa: PLR0913
self.url = url.strip().rstrip('/') if url else None
self.dep_track_service = DependencyTrackService(self.api_key, self.url, debug=debug, trace=trace, quiet=quiet)

def _json(self, project_violations: list[PolicyViolationDict]) -> Dict[str, Any]:
def _json(self, project_violations: list[PolicyViolationDict]) -> PolicyOutput:
"""
Format project violations as JSON.

Expand All @@ -181,12 +181,12 @@ def _json(self, project_violations: list[PolicyViolationDict]) -> Dict[str, Any]
Returns:
Dictionary containing JSON formatted results and summary
"""
return {
"details": json.dumps(project_violations, indent=2),
"summary": f'{len(project_violations)} policy violations were found.\n',
}
return PolicyOutput(
details= json.dumps(project_violations, indent=2),
summary= f'{len(project_violations)} policy violations were found.\n',
)

def _markdown(self, project_violations: list[PolicyViolationDict]) -> Dict[str, Any]:
def _markdown(self, project_violations: list[PolicyViolationDict]) -> PolicyOutput:
"""
Format Dependency Track violations to Markdown format.

Expand All @@ -198,7 +198,7 @@ def _markdown(self, project_violations: list[PolicyViolationDict]) -> Dict[str,
"""
return self._md_summary_generator(project_violations, generate_table)

def _jira_markdown(self, data: list[PolicyViolationDict]) -> Dict[str, Any]:
def _jira_markdown(self, data: list[PolicyViolationDict]) -> PolicyOutput:
"""
Format project violations for Jira Markdown.

Expand Down Expand Up @@ -357,8 +357,7 @@ def _set_project_id(self) -> None:
self.print_stderr(f'Error: Failed to get project uuid from: {dt_project}')
raise ValueError(f'Error: Project {self.project_name}@{self.project_version} does not have a valid UUID')

@staticmethod
def _sort_project_violations(violations: List[PolicyViolationDict]) -> List[PolicyViolationDict]:
def _sort_project_violations(self,violations: List[PolicyViolationDict]) -> List[PolicyViolationDict]:
"""
Sort project violations by priority.

Expand All @@ -377,7 +376,7 @@ def _sort_project_violations(violations: List[PolicyViolationDict]) -> List[Poli
key=lambda x: -type_priority.get(x.get('type', 'OTHER'), 1)
)

def _md_summary_generator(self, project_violations: list[PolicyViolationDict], table_generator):
def _md_summary_generator(self, project_violations: list[PolicyViolationDict], table_generator) -> PolicyOutput:
"""
Generates a Markdown summary of project policy violations.

Expand All @@ -396,10 +395,10 @@ def _md_summary_generator(self, project_violations: list[PolicyViolationDict], t
"""
if project_violations is None:
self.print_stderr('Warning: No project violations found. Returning empty results.')
return {
"details": "h3. Dependency Track Project Violations\n\nNo policy violations found.\n",
"summary": "0 policy violations were found.\n",
}
return PolicyOutput(
details= "h3. Dependency Track Project Violations\n\nNo policy violations found.\n",
summary= "0 policy violations were found.\n",
)
headers = ['State', 'Risk Type', 'Policy Name', 'Component', 'Date']
c_cols = [0, 1]
rows: List[List[str]] = []
Expand All @@ -424,11 +423,11 @@ def _md_summary_generator(self, project_violations: list[PolicyViolationDict], t
]
rows.append(row)
# End for loop
return {
"details": f'### Dependency Track Project Violations\n{table_generator(headers, rows, c_cols)}\n\n'
return PolicyOutput(
details= f'### Dependency Track Project Violations\n{table_generator(headers, rows, c_cols)}\n\n'
f'View project in Dependency Track [here]({self.url}/projects/{self.project_id}).\n',
"summary": f'{len(project_violations)} policy violations were found.\n'
}
summary= f'{len(project_violations)} policy violations were found.\n'
)

def run(self) -> int:
"""
Expand Down Expand Up @@ -470,10 +469,11 @@ def run(self) -> int:
self.print_stderr('Error: Invalid format specified.')
return PolicyStatus.ERROR.value
# Format and output data - handle empty results gracefully
data = formatter(self._sort_project_violations(dt_project_violations))
self.print_to_file_or_stdout(data['details'], self.output)
self.print_to_file_or_stderr(data['summary'], self.status)
policy_output = formatter(self._sort_project_violations(dt_project_violations))
self.print_to_file_or_stdout(policy_output.details, self.output)
self.print_to_file_or_stderr(policy_output.summary, self.status)
# Return appropriate status based on violation count
if len(dt_project_violations) > 0:
return PolicyStatus.POLICY_FAIL.value
return PolicyStatus.POLICY_SUCCESS.value

Loading