@@ -10,6 +10,26 @@ class BomBuilder
1010 def self . build ( path )
1111 original_working_directory = Dir . pwd
1212 setup ( path )
13+
14+ # If asked to validate an existing file, do not generate a new one
15+ if @options [ :validate ] && @options [ :validate_file ]
16+ content = begin
17+ File . read ( @options [ :validate_file ] )
18+ rescue StandardError => e
19+ @logger . error ( "Unable to read file for validation: #{ @options [ :validate_file ] } . #{ e . message } " )
20+ exit ( 1 )
21+ end
22+ # Use explicitly provided format if set, otherwise infer from file extension
23+ format = @options [ :bom_output_format ] || infer_format_from_path ( @options [ :validate_file ] )
24+ success , message = validate_bom_content ( content , format , @spec_version )
25+ unless success
26+ @logger . error ( message )
27+ exit ( 1 )
28+ end
29+ puts "Validation succeeded for #{ @options [ :validate_file ] } (spec #{ @spec_version } )" unless @options [ :verbose ]
30+ return
31+ end
32+
1333 specs_list
1434 bom = build_bom ( @gems , @bom_output_format , @spec_version )
1535
@@ -42,8 +62,24 @@ def self.build(path)
4262 @logger . error ( "Unable to write BOM to #{ @bom_file_path } . #{ e . message } : #{ Array ( e . backtrace ) . join ( "\n " ) } " )
4363 abort
4464 end
65+
66+ if @options [ :validate ]
67+ success , message = validate_bom_content ( bom , @bom_output_format , @spec_version )
68+ unless success
69+ @logger . error ( message )
70+ exit ( 1 )
71+ end
72+ @logger . info ( "BOM validation succeeded for spec #{ @spec_version } " ) if @options [ :verbose ]
73+ end
74+ end
75+
76+ # Infer format from file extension when not explicitly provided
77+ def self . infer_format_from_path ( path )
78+ File . extname ( path ) . downcase == '.json' ? 'json' : 'xml'
4579 end
4680
81+ private
82+
4783 def self . setup ( path )
4884 @options = { }
4985 OptionParser . new do |opts |
@@ -64,6 +100,12 @@ def self.setup(path)
64100 opts . on ( '-s' , '--spec-version version' , '(Optional) CycloneDX spec version to target (default: 1.7). Supported: 1.1, 1.2, 1.3, 1.4, 1.5, 1.6, 1.7' ) do |spec_version |
65101 @options [ :spec_version ] = spec_version
66102 end
103+ opts . on ( '--validate' , 'Validate the produced BOM against the selected CycloneDX schema' ) do
104+ @options [ :validate ] = true
105+ end
106+ opts . on ( '--validate-file PATH' , 'Validate an existing BOM file instead of generating one' ) do |path |
107+ @options [ :validate_file ] = path
108+ end
67109 opts . on_tail ( '-h' , '--help' , 'Show help message' ) do
68110 puts opts
69111 exit
@@ -86,26 +128,31 @@ def self.setup(path)
86128 licenses_file = File . read ( licenses_path )
87129 @licenses_list = JSON . parse ( licenses_file )
88130
89- if @options [ :path ] . nil?
90- @logger . error ( 'missing path to project directory' )
91- abort
92- end
131+ # If only validating a file, project path is optional; otherwise require
132+ if @options [ :validate_file ] . nil? || !@options [ :validate ]
133+ if @options [ :path ] . nil?
134+ @logger . error ( 'missing path to project directory' )
135+ abort
136+ end
93137
94- unless File . directory? ( @options [ :path ] )
95- @logger . error ( "path provided is not a valid directory. path provided was: #{ @options [ :path ] } " )
96- abort
138+ unless File . directory? ( @options [ :path ] )
139+ @logger . error ( "path provided is not a valid directory. path provided was: #{ @options [ :path ] } " )
140+ abort
141+ end
97142 end
98143
99144 # Normalize to an absolute project path to avoid relative path issues later
100- @project_path = File . expand_path ( @options [ :path ] )
145+ @project_path = File . expand_path ( @options [ :path ] ) if @options [ :path ]
101146 @provided_path = @options [ :path ]
102147
103- begin
104- @logger . info ( "Changing directory to Ruby project directory located at #{ @provided_path } " )
105- Dir . chdir @project_path
106- rescue StandardError => e
107- @logger . error ( "Unable to change directory to Ruby project directory located at #{ @provided_path } . #{ e . message } : #{ Array ( e . backtrace ) . join ( "\n " ) } " )
108- abort
148+ if @project_path
149+ begin
150+ @logger . info ( "Changing directory to Ruby project directory located at #{ @provided_path } " )
151+ Dir . chdir @project_path
152+ rescue StandardError => e
153+ @logger . error ( "Unable to change directory to Ruby project directory located at #{ @provided_path } . #{ e . message } : #{ Array ( e . backtrace ) . join ( "\n " ) } " )
154+ abort
155+ end
109156 end
110157
111158 if @options [ :bom_output_format ] . nil?
@@ -132,20 +179,22 @@ def self.setup(path)
132179 @options [ :bom_file_path ]
133180 end
134181
135- @logger . info ( "BOM will be written to #{ @bom_file_path } " )
136-
137- begin
138- # Use absolute path so it's correct regardless of current working directory
139- gemfile_path = File . join ( @project_path , 'Gemfile.lock' )
140- # Compute display path for logs: './Gemfile.lock' when provided path is '.', else '<provided>/Gemfile.lock'
141- display_gemfile_path = ( @provided_path == '.' ? './Gemfile.lock' : File . join ( @provided_path , 'Gemfile.lock' ) )
142- @logger . info ( "Parsing specs from #{ display_gemfile_path } ..." )
143- gemfile_contents = File . read ( gemfile_path )
144- @specs = Bundler ::LockfileParser . new ( gemfile_contents ) . specs
145- @logger . info ( 'Specs successfully parsed!' )
146- rescue StandardError => e
147- @logger . error ( "Unable to parse specs from #{ gemfile_path } . #{ e . message } : #{ Array ( e . backtrace ) . join ( "\n " ) } " )
148- abort
182+ @logger . info ( "BOM will be written to #{ @bom_file_path } " ) if @project_path
183+
184+ if @project_path
185+ begin
186+ # Use absolute path so it's correct regardless of current working directory
187+ gemfile_path = File . join ( @project_path , 'Gemfile.lock' )
188+ # Compute display path for logs: './Gemfile.lock' when provided path is '.', else '<provided>/Gemfile.lock'
189+ display_gemfile_path = ( @provided_path == '.' ? './Gemfile.lock' : File . join ( @provided_path , 'Gemfile.lock' ) )
190+ @logger . info ( "Parsing specs from #{ display_gemfile_path } ..." )
191+ gemfile_contents = File . read ( gemfile_path )
192+ @specs = Bundler ::LockfileParser . new ( gemfile_contents ) . specs
193+ @logger . info ( 'Specs successfully parsed!' )
194+ rescue StandardError => e
195+ @logger . error ( "Unable to parse specs from #{ gemfile_path } . #{ e . message } : #{ Array ( e . backtrace ) . join ( "\n " ) } " )
196+ abort
197+ end
149198 end
150199 end
151200
0 commit comments