scanoss 1.29.0__py3-none-any.whl → 1.31.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/cli.py CHANGED
@@ -25,6 +25,7 @@ SPDX-License-Identifier: MIT
25
25
  import argparse
26
26
  import os
27
27
  import sys
28
+ import traceback
28
29
  from dataclasses import asdict
29
30
  from pathlib import Path
30
31
  from typing import List
@@ -32,8 +33,10 @@ from typing import List
32
33
  import pypac
33
34
 
34
35
  from scanoss.cryptography import Cryptography, create_cryptography_config_from_args
35
- from scanoss.inspection.component_summary import ComponentSummary
36
- from scanoss.inspection.license_summary import LicenseSummary
36
+ from scanoss.export.dependency_track import DependencyTrackExporter
37
+ from scanoss.inspection.dependency_track.project_violation import DependencyTrackProjectViolationPolicyCheck
38
+ from scanoss.inspection.raw.component_summary import ComponentSummary
39
+ from scanoss.inspection.raw.license_summary import LicenseSummary
37
40
  from scanoss.scanners.container_scanner import (
38
41
  DEFAULT_SYFT_COMMAND,
39
42
  DEFAULT_SYFT_TIMEOUT,
@@ -64,8 +67,8 @@ from .constants import (
64
67
  from .csvoutput import CsvOutput
65
68
  from .cyclonedx import CycloneDx
66
69
  from .filecount import FileCount
67
- from .inspection.copyleft import Copyleft
68
- from .inspection.undeclared_component import UndeclaredComponent
70
+ from .inspection.raw.copyleft import Copyleft
71
+ from .inspection.raw.undeclared_component import UndeclaredComponent
69
72
  from .results import Results
70
73
  from .scancodedeps import ScancodeDeps
71
74
  from .scanner import FAST_WINNOWING, Scanner
@@ -534,76 +537,324 @@ def setup_args() -> None: # noqa: PLR0912, PLR0915
534
537
  )
535
538
  p_results.set_defaults(func=results)
536
539
 
537
- ########################################### INSPECT SUBCOMMAND ###########################################
538
- # Sub-command: inspect
540
+ # =========================================================================
541
+ # INSPECT SUBCOMMAND - Analysis and validation of scan results
542
+ # =========================================================================
543
+
544
+ # Main inspect parser - provides tools for analyzing scan results
539
545
  p_inspect = subparsers.add_parser(
540
- 'inspect', aliases=['insp', 'ins'], description=f'Inspect results: {__version__}', help='Inspect results'
546
+ 'inspect',
547
+ aliases=['insp', 'ins'],
548
+ description=f'Inspect and analyse scan results: {__version__}',
549
+ help='Inspect and analyse scan results'
541
550
  )
542
- # Sub-parser: inspect
551
+
552
+ # Inspect sub-commands parser
543
553
  p_inspect_sub = p_inspect.add_subparsers(
544
- title='Inspect Commands', dest='subparsercmd', description='Inspect sub-commands', help='Inspect sub-commands'
554
+ title='Inspect Commands',
555
+ dest='subparsercmd',
556
+ description='Available inspection sub-commands',
557
+ help='Choose an inspection type'
558
+ )
559
+
560
+ # -------------------------------------------------------------------------
561
+ # RAW RESULTS INSPECTION - Analyse raw scan output
562
+ # -------------------------------------------------------------------------
563
+
564
+ # Raw results parser - handles inspection of unprocessed scan results
565
+ p_inspect_raw = p_inspect_sub.add_parser(
566
+ 'raw',
567
+ description='Inspect and analyse SCANOSS raw scan results',
568
+ help='Analyse raw scan results for various compliance issues'
545
569
  )
546
570
 
547
- ####### INSPECT: Copyleft ######
548
- # Inspect Sub-command: inspect copyleft
549
- p_copyleft = p_inspect_sub.add_parser(
550
- 'copyleft', aliases=['cp'], description='Inspect for copyleft licenses', help='Inspect for copyleft licenses'
571
+ # Raw results sub-commands parser
572
+ p_inspect_raw_sub = p_inspect_raw.add_subparsers(
573
+ title='Raw Results Inspection Commands',
574
+ dest='subparser_subcmd',
575
+ description='Tools for analyzing raw scan results',
576
+ help='Choose a raw results analysis type'
551
577
  )
552
578
 
553
- ####### INSPECT: License Summary ######
554
- # Inspect Sub-command: inspect license summary
555
- p_license_summary = p_inspect_sub.add_parser(
556
- 'license-summary', aliases=['lic-summary', 'licsum'], description='Get license summary',
557
- help='Get detected license summary from scan results'
579
+ # Copyleft license inspection - identifies copyleft license violations
580
+ p_inspect_raw_copyleft = p_inspect_raw_sub.add_parser(
581
+ 'copyleft',
582
+ aliases=['cp'],
583
+ description='Identify components with copyleft licenses that may require compliance action',
584
+ help='Find copyleft license violations'
558
585
  )
559
586
 
560
- p_component_summary = p_inspect_sub.add_parser(
561
- 'component-summary', aliases=['comp-summary', 'compsum'], description='Get component summary',
562
- help='Get detected component summary from scan results'
587
+ # License summary inspection - provides overview of all detected licenses
588
+ p_inspect_raw_license_summary = p_inspect_raw_sub.add_parser(
589
+ 'license-summary',
590
+ aliases=['lic-summary', 'licsum'],
591
+ description='Generate comprehensive summary of all licenses found in scan results',
592
+ help='Generate license summary report'
563
593
  )
564
594
 
565
- ####### INSPECT: Undeclared components ######
566
- # Inspect Sub-command: inspect undeclared
567
- p_undeclared = p_inspect_sub.add_parser(
595
+ # Component summary inspection - provides overview of all detected components
596
+ p_inspect_raw_component_summary = p_inspect_raw_sub.add_parser(
597
+ 'component-summary',
598
+ aliases=['comp-summary', 'compsum'],
599
+ description='Generate comprehensive summary of all components found in scan results',
600
+ help='Generate component summary report'
601
+ )
602
+
603
+ # Undeclared components inspection - finds components not declared in SBOM
604
+ p_inspect_raw_undeclared = p_inspect_raw_sub.add_parser(
568
605
  'undeclared',
569
606
  aliases=['un'],
570
- description='Inspect for undeclared components',
571
- help='Inspect for undeclared components',
607
+ description='Identify components present in code but not declared in SBOM files',
608
+ help='Find undeclared components'
572
609
  )
573
- p_undeclared.add_argument(
610
+ # SBOM format option for undeclared components inspection
611
+ p_inspect_raw_undeclared.add_argument(
574
612
  '--sbom-format',
575
613
  required=False,
576
614
  choices=['legacy', 'settings'],
577
615
  default='settings',
578
- help='Sbom format for status output',
616
+ help='SBOM format type for comparison: legacy or settings (default)'
617
+ )
618
+
619
+ # -------------------------------------------------------------------------
620
+ # BACKWARD COMPATIBILITY - Support old inspect command format
621
+ # -------------------------------------------------------------------------
622
+
623
+ # Legacy copyleft inspection - backward compatibility for 'scanoss-py inspect copyleft'
624
+ p_inspect_legacy_copyleft = p_inspect_sub.add_parser(
625
+ 'copyleft',
626
+ aliases=['cp'],
627
+ description='Identify components with copyleft licenses that may require compliance action',
628
+ help='Find copyleft license violations (legacy format)'
579
629
  )
580
630
 
581
- # Add common commands for inspect copyleft and license summary
582
- for p in [p_copyleft, p_license_summary]:
631
+ # Legacy undeclared components inspection - backward compatibility for 'scanoss-py inspect undeclared'
632
+ p_inspect_legacy_undeclared = p_inspect_sub.add_parser(
633
+ 'undeclared',
634
+ aliases=['un'],
635
+ description='Identify components present in code but not declared in SBOM files',
636
+ help='Find undeclared components (legacy format)'
637
+ )
638
+
639
+ # SBOM format option for legacy undeclared components inspection
640
+ p_inspect_legacy_undeclared.add_argument(
641
+ '--sbom-format',
642
+ required=False,
643
+ choices=['legacy', 'settings'],
644
+ default='settings',
645
+ help='SBOM format type for comparison: legacy or settings (default)'
646
+ )
647
+
648
+ # Legacy license summary inspection - backward compatibility for 'scanoss-py inspect license-summary'
649
+ p_inspect_legacy_license_summary = p_inspect_sub.add_parser(
650
+ 'license-summary',
651
+ aliases=['lic-summary', 'licsum'],
652
+ description='Generate comprehensive summary of all licenses found in scan results',
653
+ help='Generate license summary report (legacy format)'
654
+ )
655
+
656
+ # Legacy component summary inspection - backward compatibility for 'scanoss-py inspect component-summary'
657
+ p_inspect_legacy_component_summary = p_inspect_sub.add_parser(
658
+ 'component-summary',
659
+ aliases=['comp-summary', 'compsum'],
660
+ description='Generate comprehensive summary of all components found in scan results',
661
+ help='Generate component summary report (legacy format)'
662
+ )
663
+
664
+ # Applies the same configuration to both legacy and raw versions
665
+ # License filtering options - common to (legacy) copyleft and license summary commands
666
+ for p in [p_inspect_raw_copyleft, p_inspect_raw_license_summary,
667
+ p_inspect_legacy_copyleft, p_inspect_legacy_license_summary]:
583
668
  p.add_argument(
584
669
  '--include',
585
- help='List of Copyleft licenses to append to the default list. Provide licenses as a comma-separated list.',
670
+ help='Additional licenses to include in analysis (comma-separated list)'
586
671
  )
587
672
  p.add_argument(
588
673
  '--exclude',
589
- help='List of Copyleft licenses to remove from default list. Provide licenses as a comma-separated list.',
674
+ help='Licenses to exclude from analysis (comma-separated list)'
590
675
  )
591
676
  p.add_argument(
592
677
  '--explicit',
593
- help='Explicit list of Copyleft licenses to consider. Provide licenses as a comma-separated list.s',
678
+ help='Use only these specific licenses for analysis (comma-separated list)'
594
679
  )
595
680
 
596
- # Add common commands for inspect copyleft and license summary
597
- for p in [p_license_summary, p_component_summary]:
598
- p.add_argument('-i', '--input', nargs='?', help='Path to results file')
599
- p.add_argument('-o', '--output', type=str, help='Save summary into a file')
681
+ # Common options for (legacy) copyleft and undeclared component inspection
682
+ for p in [p_inspect_raw_copyleft, p_inspect_raw_undeclared, p_inspect_legacy_copyleft, p_inspect_legacy_undeclared]:
683
+ p.add_argument(
684
+ '-i', '--input',
685
+ nargs='?',
686
+ help='Path to scan results file to analyse'
687
+ )
688
+ p.add_argument(
689
+ '-f', '--format',
690
+ required=False,
691
+ choices=['json', 'md', 'jira_md'],
692
+ default='json',
693
+ help='Output format: json (default), md (Markdown), or jira_md (JIRA Markdown)'
694
+ )
695
+ p.add_argument(
696
+ '-o', '--output',
697
+ type=str,
698
+ help='Save detailed results to specified file'
699
+ )
700
+ p.add_argument(
701
+ '-s', '--status',
702
+ type=str,
703
+ help='Save summary status report to Markdown file'
704
+ )
600
705
 
601
- p_undeclared.set_defaults(func=inspect_undeclared)
602
- p_copyleft.set_defaults(func=inspect_copyleft)
603
- p_license_summary.set_defaults(func=inspect_license_summary)
604
- p_component_summary.set_defaults(func=inspect_component_summary)
706
+ # Common options for (legacy) license and component summary commands
707
+ for p in [p_inspect_raw_license_summary, p_inspect_raw_component_summary,
708
+ p_inspect_legacy_license_summary, p_inspect_legacy_component_summary]:
709
+ p.add_argument(
710
+ '-i', '--input',
711
+ nargs='?',
712
+ help='Path to scan results file to analyse'
713
+ )
714
+ p.add_argument(
715
+ '-o', '--output',
716
+ type=str,
717
+ help='Save summary report to specified file'
718
+ )
605
719
 
606
- ########################################### END INSPECT SUBCOMMAND ###########################################
720
+ # -------------------------------------------------------------------------
721
+ # DEPENDENCY TRACK INSPECTION - Analyse Dependency Track project data
722
+ # -------------------------------------------------------------------------
723
+
724
+ # Dependency Track parser - handles inspection of DT project status and violations
725
+ p_dep_track_sub = p_inspect_sub.add_parser(
726
+ 'dependency-track',
727
+ aliases=['dt'],
728
+ description='Inspect and analyse Dependency Track project status and policy violations',
729
+ help='Analyse Dependency Track projects'
730
+ )
731
+
732
+ # Dependency Track sub-commands parser
733
+ p_inspect_dep_track_sub = p_dep_track_sub.add_subparsers(
734
+ title='Dependency Track Inspection Commands',
735
+ dest='subparser_subcmd',
736
+ description='Tools for analysing Dependency Track project data',
737
+ help='Choose a Dependency Track analysis type'
738
+ )
739
+
740
+ # Project violations inspection - analyses policy violations in DT projects
741
+ p_inspect_dt_project_violation = p_inspect_dep_track_sub.add_parser(
742
+ 'project-violations',
743
+ aliases=['pv'],
744
+ description='Analyse policy violations and compliance issues in Dependency Track projects',
745
+ help='Inspect project policy violations'
746
+ )
747
+ # Dependency Track connection and authentication options
748
+ p_inspect_dt_project_violation.add_argument(
749
+ '--url',
750
+ required=True,
751
+ type=str,
752
+ help='Dependency Track server base URL (e.g., https://dtrack.example.com)'
753
+ )
754
+ p_inspect_dt_project_violation.add_argument(
755
+ '--upload-token', '-ut',
756
+ required=False,
757
+ type=str,
758
+ help='Project-specific upload token for accessing DT project data'
759
+ )
760
+ p_inspect_dt_project_violation.add_argument(
761
+ '--project-id', '-pid',
762
+ required=False,
763
+ type=str,
764
+ help='Dependency Track project UUID to inspect'
765
+ )
766
+ p_inspect_dt_project_violation.add_argument(
767
+ '--apikey', '-k',
768
+ required=True,
769
+ type=str,
770
+ help='Dependency Track API key for authentication'
771
+ )
772
+ p_inspect_dt_project_violation.add_argument(
773
+ '--project-name', '-pn',
774
+ required=False,
775
+ type=str,
776
+ help='Dependency Track project name'
777
+ )
778
+ p_inspect_dt_project_violation.add_argument(
779
+ '--project-version', '-pv',
780
+ required=False,
781
+ type=str,
782
+ help='Dependency Track project version'
783
+ )
784
+ p_inspect_dt_project_violation.add_argument(
785
+ '--output', '-o',
786
+ required=False,
787
+ type=str,
788
+ help='Save inspection results to specified file'
789
+ )
790
+ p_inspect_dt_project_violation.add_argument(
791
+ '--status',
792
+ required=False,
793
+ type=str,
794
+ help='Save summary status report to specified file'
795
+ )
796
+ p_inspect_dt_project_violation.add_argument(
797
+ '--format', '-f',
798
+ required=False,
799
+ choices=['json', 'md'],
800
+ default='json',
801
+ help='Output format: json (default) or md (Markdown)'
802
+ )
803
+ p_inspect_dt_project_violation.add_argument(
804
+ '--timeout', '-M',
805
+ required=False,
806
+ default='300',
807
+ help='Timeout (in seconds) for API communication (optional - default 300 sec)'
808
+ )
809
+
810
+ # TODO Move to the command call def location
811
+ # RAW results
812
+ p_inspect_raw_undeclared.set_defaults(func=inspect_undeclared)
813
+ p_inspect_raw_copyleft.set_defaults(func=inspect_copyleft)
814
+ p_inspect_raw_license_summary.set_defaults(func=inspect_license_summary)
815
+ p_inspect_raw_component_summary.set_defaults(func=inspect_component_summary)
816
+ # Legacy backward compatibility commands
817
+ p_inspect_legacy_copyleft.set_defaults(func=inspect_copyleft)
818
+ p_inspect_legacy_undeclared.set_defaults(func=inspect_undeclared)
819
+ p_inspect_legacy_license_summary.set_defaults(func=inspect_license_summary)
820
+ p_inspect_legacy_component_summary.set_defaults(func=inspect_component_summary)
821
+ # Dependency Track
822
+ p_inspect_dt_project_violation.set_defaults(func=inspect_dep_track_project_violations)
823
+
824
+ # =========================================================================
825
+ # END INSPECT SUBCOMMAND CONFIGURATION
826
+ # =========================================================================
827
+
828
+ # Sub-command: export
829
+ p_export = subparsers.add_parser(
830
+ 'export',
831
+ aliases=['exp'],
832
+ description=f'Export SBOM files to external platforms: {__version__}',
833
+ help='Export SBOM files to external platforms',
834
+ )
835
+
836
+ export_sub = p_export.add_subparsers(
837
+ title='Export Commands',
838
+ dest='subparsercmd',
839
+ description='export sub-commands',
840
+ help='export sub-commands',
841
+ )
842
+
843
+ # Export Sub-command: export dt (Dependency Track)
844
+ e_dt = export_sub.add_parser(
845
+ 'dt',
846
+ aliases=['dependency-track'],
847
+ description='Export SBOM to Dependency Track',
848
+ help='Upload SBOM files to Dependency Track',
849
+ )
850
+ e_dt.add_argument('-i', '--input', type=str, required=True, help='Input SBOM file (CycloneDX JSON format)')
851
+ e_dt.add_argument('--url', type=str, required=True, help='Dependency Track base URL')
852
+ e_dt.add_argument('--apikey', '-k', type=str, required=True, help='Dependency Track API key')
853
+ e_dt.add_argument('--output', '-o', type=str, help='File to save export token and uuid into')
854
+ e_dt.add_argument('--project-id', '-pid', type=str, help='Dependency Track project UUID')
855
+ e_dt.add_argument('--project-name', '-pn', type=str, help='Dependency Track project name')
856
+ e_dt.add_argument('--project-version', '-pv', type=str, help='Dependency Track project version')
857
+ e_dt.set_defaults(func=export_dt)
607
858
 
608
859
  # Sub-command: folder-scan
609
860
  p_folder_scan = subparsers.add_parser(
@@ -707,19 +958,6 @@ def setup_args() -> None: # noqa: PLR0912, PLR0915
707
958
  help='Skip default settings file (scanoss.json) if it exists',
708
959
  )
709
960
 
710
- for p in [p_copyleft, p_undeclared]:
711
- p.add_argument('-i', '--input', nargs='?', help='Path to results file')
712
- p.add_argument(
713
- '-f',
714
- '--format',
715
- required=False,
716
- choices=['json', 'md', 'jira_md'],
717
- default='json',
718
- help='Output format (default: json)',
719
- )
720
- p.add_argument('-o', '--output', type=str, help='Save details into a file')
721
- p.add_argument('-s', '--status', type=str, help='Save summary data into Markdown file')
722
-
723
961
  # Global Scan command options
724
962
  for p in [p_scan, p_cs]:
725
963
  p.add_argument(
@@ -847,10 +1085,15 @@ def setup_args() -> None: # noqa: PLR0912, PLR0915
847
1085
  c_versions,
848
1086
  c_semgrep,
849
1087
  p_results,
850
- p_undeclared,
851
- p_copyleft,
852
- p_license_summary,
853
- p_component_summary,
1088
+ p_inspect_raw_undeclared,
1089
+ p_inspect_raw_copyleft,
1090
+ p_inspect_raw_license_summary,
1091
+ p_inspect_raw_component_summary,
1092
+ p_inspect_legacy_copyleft,
1093
+ p_inspect_legacy_undeclared,
1094
+ p_inspect_legacy_license_summary,
1095
+ p_inspect_legacy_component_summary,
1096
+ p_inspect_dt_project_violation,
854
1097
  c_provenance,
855
1098
  p_folder_scan,
856
1099
  p_folder_hash,
@@ -858,6 +1101,7 @@ def setup_args() -> None: # noqa: PLR0912, PLR0915
858
1101
  p_crypto_algorithms,
859
1102
  p_crypto_hints,
860
1103
  p_crypto_versions_in_range,
1104
+ e_dt,
861
1105
  ]:
862
1106
  p.add_argument('--debug', '-d', action='store_true', help='Enable debug messages')
863
1107
  p.add_argument('--trace', '-t', action='store_true', help='Enable trace messages, including API posts')
@@ -871,10 +1115,14 @@ def setup_args() -> None: # noqa: PLR0912, PLR0915
871
1115
  parser.print_help() # No sub command subcommand, print general help
872
1116
  sys.exit(1)
873
1117
  elif (
874
- args.subparser in ('utils', 'ut', 'component', 'comp', 'inspect', 'insp', 'ins', 'crypto', 'cr')
1118
+ args.subparser
1119
+ in ('utils', 'ut', 'component', 'comp', 'inspect', 'insp', 'ins', 'crypto', 'cr', 'export', 'exp')
875
1120
  ) and not args.subparsercmd:
876
1121
  parser.parse_args([args.subparser, '--help']) # Force utils helps to be displayed
877
1122
  sys.exit(1)
1123
+ elif (args.subparser in 'inspect') and (args.subparsercmd in ('raw', 'dt')) and (args.subparser_subcmd is None):
1124
+ parser.parse_args([args.subparser, args.subparsercmd, '--help']) # Force utils helps to be displayed
1125
+ sys.exit(1)
878
1126
  args.func(parser, args) # Execute the function associated with the sub-command
879
1127
 
880
1128
 
@@ -908,16 +1156,14 @@ def file_count(parser, args):
908
1156
  print_stderr('Please specify a folder')
909
1157
  parser.parse_args([args.subparser, '-h'])
910
1158
  sys.exit(1)
911
- scan_output: str = None
912
1159
  if args.output:
913
- scan_output = args.output
914
- open(scan_output, 'w').close()
1160
+ initialise_empty_file(args.output)
915
1161
 
916
1162
  counter = FileCount(
917
1163
  debug=args.debug,
918
1164
  quiet=args.quiet,
919
1165
  trace=args.trace,
920
- scan_output=scan_output,
1166
+ scan_output=args.output,
921
1167
  hidden_files_folders=args.all_hidden,
922
1168
  )
923
1169
  if not os.path.exists(args.scan_dir):
@@ -946,10 +1192,8 @@ def wfp(parser, args):
946
1192
  sys.exit(1)
947
1193
  if args.strip_hpsm and not args.hpsm and not args.quiet:
948
1194
  print_stderr('Warning: --strip-hpsm option supplied without enabling HPSM (--hpsm). Ignoring.')
949
- scan_output: str = None
950
1195
  if args.output:
951
- scan_output = args.output
952
- open(scan_output, 'w').close()
1196
+ initialise_empty_file(args.output)
953
1197
 
954
1198
  # Load scan settings
955
1199
  scan_settings = None
@@ -982,15 +1226,15 @@ def wfp(parser, args):
982
1226
  )
983
1227
  if args.stdin:
984
1228
  contents = sys.stdin.buffer.read()
985
- scanner.wfp_contents(args.stdin, contents, scan_output)
1229
+ scanner.wfp_contents(args.stdin, contents, args.output)
986
1230
  elif args.scan_dir:
987
1231
  if not os.path.exists(args.scan_dir):
988
1232
  print_stderr(f'Error: File or folder specified does not exist: {args.scan_dir}.')
989
1233
  sys.exit(1)
990
1234
  if os.path.isdir(args.scan_dir):
991
- scanner.wfp_folder(args.scan_dir, scan_output)
1235
+ scanner.wfp_folder(args.scan_dir, args.output)
992
1236
  elif os.path.isfile(args.scan_dir):
993
- scanner.wfp_file(args.scan_dir, scan_output)
1237
+ scanner.wfp_file(args.scan_dir, args.output)
994
1238
  else:
995
1239
  print_stderr(f'Error: Path specified is neither a file or a folder: {args.scan_dir}.')
996
1240
  sys.exit(1)
@@ -1087,10 +1331,8 @@ def scan(parser, args): # noqa: PLR0912, PLR0915
1087
1331
  if args.strip_hpsm and not args.hpsm and not args.quiet:
1088
1332
  print_stderr('Warning: --strip-hpsm option supplied without enabling HPSM (--hpsm). Ignoring.')
1089
1333
 
1090
- scan_output: str = None
1091
1334
  if args.output:
1092
- scan_output = args.output
1093
- open(scan_output, 'w').close()
1335
+ initialise_empty_file(args.output)
1094
1336
  output_format = args.format if args.format else 'plain'
1095
1337
  flags = args.flags if args.flags else None
1096
1338
  if args.debug and not args.quiet:
@@ -1145,7 +1387,7 @@ def scan(parser, args): # noqa: PLR0912, PLR0915
1145
1387
  quiet=args.quiet,
1146
1388
  api_key=args.key,
1147
1389
  url=args.apiurl,
1148
- scan_output=scan_output,
1390
+ scan_output=args.output,
1149
1391
  output_format=output_format,
1150
1392
  flags=flags,
1151
1393
  nb_threads=args.threads,
@@ -1257,16 +1499,15 @@ def dependency(parser, args):
1257
1499
  if not os.path.exists(args.scan_loc):
1258
1500
  print_stderr(f'Error: File or folder specified does not exist: {args.scan_loc}.')
1259
1501
  sys.exit(1)
1260
- scan_output: str = None
1261
1502
  if args.output:
1262
- scan_output = args.output
1263
- open(scan_output, 'w').close()
1503
+ initialise_empty_file(args.output)
1264
1504
 
1265
1505
  sc_deps = ScancodeDeps(
1266
1506
  debug=args.debug, quiet=args.quiet, trace=args.trace, sc_command=args.sc_command, timeout=args.sc_timeout
1267
1507
  )
1268
- if not sc_deps.get_dependencies(what_to_scan=args.scan_loc, result_output=scan_output):
1508
+ if not sc_deps.get_dependencies(what_to_scan=args.scan_loc, result_output=args.output):
1269
1509
  sys.exit(1)
1510
+ return None
1270
1511
 
1271
1512
 
1272
1513
  def convert(parser, args):
@@ -1304,143 +1545,341 @@ def convert(parser, args):
1304
1545
  if not success:
1305
1546
  sys.exit(1)
1306
1547
 
1307
- ################################ INSPECT handlers ################################
1548
+
1549
+ # =============================================================================
1550
+ # INSPECT COMMAND HANDLERS - Functions that execute inspection operations
1551
+ # =============================================================================
1552
+
1308
1553
  def inspect_copyleft(parser, args):
1309
1554
  """
1310
- Run the "inspect" sub-command
1555
+ Handle copyleft license inspection command.
1556
+
1557
+ Analyses scan results to identify components using copyleft licenses
1558
+ that may require compliance actions such as source code disclosure.
1559
+
1311
1560
  Parameters
1312
1561
  ----------
1313
- parser: ArgumentParser
1314
- command line parser object
1315
- args: Namespace
1316
- Parsed arguments
1562
+ parser : ArgumentParser
1563
+ Command line parser object for help display
1564
+ args : Namespace
1565
+ Parsed command line arguments containing:
1566
+ - input: Path to scan results file
1567
+ - output: Optional output file path
1568
+ - status: Optional status summary file path
1569
+ - format: Output format (json, md, jira_md)
1570
+ - include/exclude/explicit: License filter options
1317
1571
  """
1572
+ # Validate required input file parameter
1318
1573
  if args.input is None:
1319
- print_stderr('Please specify an input file to inspect')
1320
- parser.parse_args([args.subparser, args.subparsercmd, '-h'])
1574
+ print_stderr('ERROR: Input file is required for copyleft inspection')
1575
+ parser.parse_args([args.subparser, args.subparsercmd, args.subparser_subcmd, '-h'])
1321
1576
  sys.exit(1)
1322
- output: str = None
1577
+ # Initialise output file if specified
1323
1578
  if args.output:
1324
- output = args.output
1325
- open(output, 'w').close()
1326
-
1327
- status_output: str = None
1579
+ initialise_empty_file(args.output)
1580
+ # Initialise status summary file if specified
1328
1581
  if args.status:
1329
- status_output = args.status
1330
- open(status_output, 'w').close()
1582
+ initialise_empty_file(args.status)
1583
+ try:
1584
+ # Create and configure copyleft inspector
1585
+ i_copyleft = Copyleft(
1586
+ debug=args.debug,
1587
+ trace=args.trace,
1588
+ quiet=args.quiet,
1589
+ filepath=args.input,
1590
+ format_type=args.format,
1591
+ status=args.status,
1592
+ output=args.output,
1593
+ include=args.include, # Additional licenses to check
1594
+ exclude=args.exclude, # Licenses to ignore
1595
+ explicit=args.explicit, # Explicit license list
1596
+ )
1331
1597
 
1332
- i_copyleft = Copyleft(
1333
- debug=args.debug,
1334
- trace=args.trace,
1335
- quiet=args.quiet,
1336
- filepath=args.input,
1337
- format_type=args.format,
1338
- status=status_output,
1339
- output=output,
1340
- include=args.include,
1341
- exclude=args.exclude,
1342
- explicit=args.explicit,
1343
- )
1344
- status, _ = i_copyleft.run()
1345
- sys.exit(status)
1598
+ # Execute inspection and exit with appropriate status code
1599
+ status, _ = i_copyleft.run()
1600
+ sys.exit(status)
1601
+ except Exception as e:
1602
+ print_stderr(e)
1603
+ if args.debug:
1604
+ traceback.print_exc()
1605
+ sys.exit(1)
1346
1606
 
1347
1607
 
1348
1608
  def inspect_undeclared(parser, args):
1349
1609
  """
1350
- Run the "inspect" sub-command
1610
+ Handle undeclared components inspection command.
1611
+
1612
+ Analyses scan results to identify components that are present in the
1613
+ codebase but not declared in SBOM or manifest files, which may indicate
1614
+ security or compliance risks.
1615
+
1351
1616
  Parameters
1352
1617
  ----------
1353
- parser: ArgumentParser
1354
- command line parser object
1355
- args: Namespace
1356
- Parsed arguments
1618
+ parser : ArgumentParser
1619
+ Command line parser object for help display
1620
+ args : Namespace
1621
+ Parsed command line arguments containing:
1622
+ - input: Path to scan results file
1623
+ - output: Optional output file path
1624
+ - status: Optional status summary file path
1625
+ - format: Output format (json, md, jira_md)
1626
+ - sbom_format: SBOM format type (legacy, settings)
1357
1627
  """
1628
+ # Validate required input file parameter
1358
1629
  if args.input is None:
1359
- print_stderr('Please specify an input file to inspect')
1360
- parser.parse_args([args.subparser, args.subparsercmd, '-h'])
1630
+ print_stderr('ERROR: Input file is required for undeclared component inspection')
1631
+ parser.parse_args([args.subparser, args.subparsercmd, args.subparser_subcmd, '-h'])
1361
1632
  sys.exit(1)
1362
- output: str = None
1633
+
1634
+ # Initialise output file if specified
1363
1635
  if args.output:
1364
- output = args.output
1365
- open(output, 'w').close()
1636
+ initialise_empty_file(args.output)
1366
1637
 
1367
- status_output: str = None
1638
+ # Initialise status summary file if specified
1368
1639
  if args.status:
1369
- status_output = args.status
1370
- open(status_output, 'w').close()
1371
- i_undeclared = UndeclaredComponent(
1372
- debug=args.debug,
1373
- trace=args.trace,
1374
- quiet=args.quiet,
1375
- filepath=args.input,
1376
- format_type=args.format,
1377
- status=status_output,
1378
- output=output,
1379
- sbom_format=args.sbom_format,
1380
- )
1381
- status, _ = i_undeclared.run()
1382
- sys.exit(status)
1640
+ initialise_empty_file(args.status)
1641
+
1642
+ try:
1643
+ # Create and configure undeclared component inspector
1644
+ i_undeclared = UndeclaredComponent(
1645
+ debug=args.debug,
1646
+ trace=args.trace,
1647
+ quiet=args.quiet,
1648
+ filepath=args.input,
1649
+ format_type=args.format,
1650
+ status=args.status,
1651
+ output=args.output,
1652
+ sbom_format=args.sbom_format, # Format for SBOM comparison
1653
+ )
1654
+
1655
+ # Execute inspection and exit with appropriate status code
1656
+ status, _ = i_undeclared.run()
1657
+ sys.exit(status)
1658
+ except Exception as e:
1659
+ print_stderr(e)
1660
+ if args.debug:
1661
+ traceback.print_exc()
1662
+ sys.exit(1)
1663
+
1383
1664
 
1384
1665
  def inspect_license_summary(parser, args):
1385
1666
  """
1386
- Run the "inspect" sub-command
1387
- Parameters
1388
- ----------
1389
- parser: ArgumentParser
1390
- command line parser object
1391
- args: Namespace
1392
- Parsed arguments
1393
- """
1667
+ Handle license summary inspection command.
1668
+
1669
+ Generates comprehensive summary of all licenses detected in scan results,
1670
+ including license counts, risk levels, and compliance recommendations.
1671
+
1672
+ Parameters
1673
+ ----------
1674
+ parser : ArgumentParser
1675
+ Command line parser object for help display
1676
+ args : Namespace
1677
+ Parsed command line arguments containing:
1678
+ - input: Path to scan results file
1679
+ - output: Optional output file path
1680
+ - include/exclude/explicit: License filter options
1681
+ """
1682
+ # Validate required input file parameter
1394
1683
  if args.input is None:
1395
- print_stderr('Please specify an input file to inspect')
1396
- parser.parse_args([args.subparser, args.subparsercmd, '-h'])
1684
+ print_stderr('ERROR: Input file is required for license summary')
1685
+ parser.parse_args([args.subparser, args.subparsercmd, args.subparser_subcmd, '-h'])
1397
1686
  sys.exit(1)
1398
- output: str = None
1687
+
1688
+ # Initialise output file if specified
1399
1689
  if args.output:
1400
- output = args.output
1401
- open(output, 'w').close()
1690
+ initialise_empty_file(args.output)
1402
1691
 
1692
+ # Create and configure license summary generator
1403
1693
  i_license_summary = LicenseSummary(
1404
1694
  debug=args.debug,
1405
1695
  trace=args.trace,
1406
1696
  quiet=args.quiet,
1407
1697
  filepath=args.input,
1408
- output=output,
1409
- include=args.include,
1410
- exclude=args.exclude,
1411
- explicit=args.explicit,
1698
+ output=args.output,
1699
+ include=args.include, # Additional licenses to include
1700
+ exclude=args.exclude, # Licenses to exclude from summary
1701
+ explicit=args.explicit, # Explicit license list to summarize
1412
1702
  )
1413
- i_license_summary.run()
1703
+ try:
1704
+ # Execute summary generation
1705
+ i_license_summary.run()
1706
+ except Exception as e:
1707
+ print_stderr(e)
1708
+ if args.debug:
1709
+ traceback.print_exc()
1710
+ sys.exit(1)
1414
1711
 
1415
1712
  def inspect_component_summary(parser, args):
1416
1713
  """
1417
- Run the "inspect" sub-command
1418
- Parameters
1419
- ----------
1420
- parser: ArgumentParser
1421
- command line parser object
1422
- args: Namespace
1423
- Parsed arguments
1424
- """
1714
+ Handle component summary inspection command.
1715
+
1716
+ Generates a comprehensive summary of all components detected in scan results,
1717
+ including component counts, versions, match types, and security information.
1718
+
1719
+ Parameters
1720
+ ----------
1721
+ parser : ArgumentParser
1722
+ Command line parser object for help display
1723
+ args : Namespace
1724
+ Parsed command line arguments containing:
1725
+ - input: Path to scan results file
1726
+ - output: Optional output file path
1727
+ """
1728
+ # Validate required input file parameter
1425
1729
  if args.input is None:
1426
- print_stderr('Please specify an input file to inspect')
1427
- parser.parse_args([args.subparser, args.subparsercmd, '-h'])
1730
+ print_stderr('ERROR: Input file is required for component summary')
1731
+ parser.parse_args([args.subparser, args.subparsercmd, args.subparser_subcmd, '-h'])
1428
1732
  sys.exit(1)
1429
- output: str = None
1733
+
1734
+ # Initialise an output file if specified
1430
1735
  if args.output:
1431
- output = args.output
1432
- open(output, 'w').close()
1736
+ initialise_empty_file(args.output) # Create/clear output file
1433
1737
 
1738
+ # Create and configure component summary generator
1434
1739
  i_component_summary = ComponentSummary(
1435
1740
  debug=args.debug,
1436
1741
  trace=args.trace,
1437
1742
  quiet=args.quiet,
1438
1743
  filepath=args.input,
1439
- output=output,
1744
+ output=args.output,
1440
1745
  )
1441
- i_component_summary.run()
1442
1746
 
1443
- ################################ End inspect handlers ################################
1747
+ try:
1748
+ # Execute summary generation
1749
+ i_component_summary.run()
1750
+ except Exception as e:
1751
+ print_stderr(e)
1752
+ if args.debug:
1753
+ traceback.print_exc()
1754
+ sys.exit(1)
1755
+
1756
+ def inspect_dep_track_project_violations(parser, args):
1757
+ """
1758
+ Handle Dependency Track project inspection command.
1759
+
1760
+ Analyses Dependency Track projects for policy violations, security issues,
1761
+ and compliance status. Connects to DT API to retrieve project data and
1762
+ generate detailed violation reports.
1763
+
1764
+ Parameters
1765
+ ----------
1766
+ parser : ArgumentParser
1767
+ Command line parser object for help display
1768
+ args : Namespace
1769
+ Parsed command line arguments containing:
1770
+ - url: Dependency Track base URL
1771
+ - apikey: API key for authentication
1772
+ - project_id: Project UUID to inspect
1773
+ - project_name: Project name to inspect
1774
+ - project_version: Project version to inspect
1775
+ - upload_token: Upload token for project access
1776
+ - output: Optional output file path
1777
+ - format: Output format (json, md)
1778
+ - timeout: Optional timeout for API requests
1779
+
1780
+ """
1781
+ # Make sure we have project id/project name and version
1782
+ _dt_args_validator(parser, args)
1783
+ # Initialise the output file if specified
1784
+ if args.output:
1785
+ initialise_empty_file(args.output)
1786
+ # Create and configure Dependency Track inspector
1787
+ try:
1788
+ dt_proj_violations = DependencyTrackProjectViolationPolicyCheck(
1789
+ debug=args.debug,
1790
+ trace=args.trace,
1791
+ quiet=args.quiet,
1792
+ output=args.output,
1793
+ status= args.status,
1794
+ format_type=args.format,
1795
+ url=args.url, # DT server URL
1796
+ api_key=args.apikey, # Authentication key
1797
+ project_id=args.project_id, # Target project UUID
1798
+ upload_token=args.upload_token, # Upload access token
1799
+ project_name=args.project_name, # DT project name
1800
+ project_version=args.project_version, # DT project version
1801
+ timeout=args.timeout,
1802
+ )
1803
+
1804
+ # Execute inspection and exit with appropriate status code
1805
+ status, _ = dt_proj_violations.run() #TODO remove datastructure from return
1806
+ sys.exit(status)
1807
+ except Exception as e:
1808
+ print_stderr(e)
1809
+ if args.debug:
1810
+ traceback.print_exc()
1811
+ sys.exit(1)
1812
+
1813
+
1814
+ # =============================================================================
1815
+ # END INSPECT COMMAND HANDLERS
1816
+ # =============================================================================
1817
+
1818
+ def export_dt(parser, args):
1819
+ """
1820
+ Validates and exports a Software Bill of Materials (SBOM) to a Dependency-Track server.
1821
+
1822
+ Parameters:
1823
+ parser (argparse.ArgumentParser): The argument parser to validate input arguments.
1824
+ args (argparse.Namespace): Parsed arguments passed to the command.
1825
+
1826
+ Raises:
1827
+ SystemExit: If argument validation fails or uploading the SBOM to the Dependency-Track server
1828
+ is unsuccessful.
1829
+ """
1830
+ # Make sure we have project id/project name and version
1831
+ _dt_args_validator(parser, args)
1832
+ if args.output:
1833
+ initialise_empty_file(args.output)
1834
+ if not args.quiet:
1835
+ print_stderr(f'Outputting export data result to: {args.output}')
1836
+ try:
1837
+ dt_exporter = DependencyTrackExporter(
1838
+ url=args.url,
1839
+ apikey=args.apikey,
1840
+ output=args.output,
1841
+ debug=args.debug,
1842
+ trace=args.trace,
1843
+ quiet=args.quiet,
1844
+ )
1845
+ success = dt_exporter.upload_sbom_file(args.input, args.project_id, args.project_name,
1846
+ args.project_version, args.output)
1847
+ if not success:
1848
+ sys.exit(1)
1849
+ except Exception as e:
1850
+ print_stderr(f'ERROR: {e}')
1851
+ if args.debug:
1852
+ traceback.print_exc()
1853
+ sys.exit(1)
1854
+
1855
+ def _dt_args_validator(parser, args):
1856
+ """
1857
+ Validates command-line arguments related to project identification.
1858
+
1859
+ Parameters
1860
+ ----------
1861
+ parser : argparse.ArgumentParser
1862
+ An argument parser instance for handling command-line arguments.
1863
+ args : argparse.Namespace
1864
+ Parsed arguments from the command line containing project-related information.
1865
+
1866
+ Raises
1867
+ ------
1868
+ SystemExit
1869
+ If neither a project ID nor the required combination of project name and
1870
+ project version is provided, or if any of the compulsory arguments
1871
+ are missing.
1872
+ """
1873
+ if not args.project_id and not args.project_name and not args.project_version:
1874
+ print_stderr(
1875
+ 'Please specify either a project ID (--project-id) or a project name (--project-name) and '
1876
+ 'version (--project-version)'
1877
+ )
1878
+ parser.parse_args([args.subparser, '-h'])
1879
+ sys.exit(1)
1880
+ if not args.project_id and (not args.project_name or not args.project_version):
1881
+ print_stderr('Please supply a project name (--project-name) and version (--project-version)')
1882
+ sys.exit(1)
1444
1883
 
1445
1884
  def utils_certloc(*_):
1446
1885
  """
@@ -2045,7 +2484,31 @@ def get_scanoss_settings_from_args(args):
2045
2484
  except ScanossSettingsError as e:
2046
2485
  print_stderr(f'Error: {e}')
2047
2486
  sys.exit(1)
2048
- return scanoss_settings
2487
+ return scanoss_settings
2488
+
2489
+
2490
+ def initialise_empty_file(filename: str):
2491
+ """
2492
+ Initialises an empty file with the specified name. If the file already exists,
2493
+ it truncates its content. Ensures proper error handling in case of failure.
2494
+
2495
+ Args:
2496
+ filename (str): The name of the file to be initialised.
2497
+
2498
+ Raises:
2499
+ SystemExit: If the file cannot be created or written due to an exception,
2500
+ the function prints an error message and exits the program.
2501
+
2502
+ Note:
2503
+ This function writes an empty file and handles exceptions to ensure the
2504
+ program does not continue execution in case of an error.
2505
+ """
2506
+ if filename:
2507
+ try:
2508
+ open(filename, 'w').close()
2509
+ except Exception as e:
2510
+ print_stderr(f'Error: Unable to create output file {filename}: {e}')
2511
+ sys.exit(1)
2049
2512
 
2050
2513
 
2051
2514
  def main():