Skip to content

Commit a3c93a0

Browse files
authored
Merge pull request #83 from CycloneDX/validation
Add validate command for SBOM validation
2 parents 886a55e + 79bca05 commit a3c93a0

File tree

11 files changed

+6536
-0
lines changed

11 files changed

+6536
-0
lines changed

README.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ Commands:
1818
convert Convert between different SBOM formats
1919
diff <from-file> <to-file> Generate an SBOM diff
2020
merge Merge two or more SBOMs
21+
validate Validate an SBOM
2122
```
2223

2324
The CycloneDX CLI tool currently supports SBOM analysis, diffing, merging and format conversions.
@@ -94,6 +95,21 @@ Options:
9495
--component-versions Report component versions that have been added, removed or modified.
9596
```
9697

98+
## Validate Command
99+
100+
```
101+
validate:
102+
Validate an SBOM
103+
104+
Usage:
105+
cyclonedx validate [options]
106+
107+
Options:
108+
--input-file <input-file> Input SBOM filename, will read from stdin if no value provided.
109+
--input-format <autodetect|json|json_v1_2|xml|xml_v1_0|xml_v1_1|xml_v1_2> Specify input file format.
110+
--fail-on-errors Fail on validation errors (return a non-zero exit code)
111+
```
112+
97113
## Docker Image
98114

99115
The CycloneDX CLI tool can also be run using docker `docker run cyclonedx/cyclonedx-cli`.
Lines changed: 213 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,213 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.CommandLine;
4+
using System.CommandLine.Invocation;
5+
using System.IO;
6+
using System.Linq;
7+
using System.Reflection;
8+
using System.Text;
9+
using System.Text.Json;
10+
using System.Threading.Tasks;
11+
using System.Xml;
12+
using System.Xml.Schema;
13+
using System.Xml.XPath;
14+
using Json.Schema;
15+
using CycloneDX.Models.v1_2;
16+
using CycloneDX.Json;
17+
using CycloneDX.CLI.Commands;
18+
using CycloneDX.CLI.Models;
19+
20+
namespace CycloneDX.CLI
21+
{
22+
internal class ValidateCommand
23+
{
24+
public enum InputFormat
25+
{
26+
autodetect,
27+
json,
28+
json_v1_2,
29+
xml,
30+
xml_v1_2,
31+
xml_v1_1,
32+
xml_v1_0,
33+
}
34+
35+
public class Options
36+
{
37+
public string InputFile { get; set; }
38+
public InputFormat InputFormat { get; set; }
39+
public bool FailOnErrors { get; set; }
40+
}
41+
42+
internal static void Configure(RootCommand rootCommand)
43+
{
44+
var subCommand = new Command("validate", "Validate an SBOM");
45+
subCommand.Add(new Option<string>("--input-file", "Input SBOM filename, will read from stdin if no value provided."));
46+
subCommand.Add(new Option<InputFormat>("--input-format", "Specify input file format."));
47+
subCommand.Add(new Option<bool>("--fail-on-errors", "Fail on validation errors (return a non-zero exit code)"));
48+
subCommand.Handler = CommandHandler.Create<Options>(Validate);
49+
rootCommand.Add(subCommand);
50+
}
51+
52+
public static async Task<int> Validate(Options options)
53+
{
54+
ValidateInputFormat(options);
55+
if (options.InputFormat == InputFormat.autodetect)
56+
{
57+
Console.Error.WriteLine("Unable to auto-detect input format");
58+
return (int)ExitCode.ParameterValidationError;
59+
}
60+
61+
var inputBom = ReadInput(options);
62+
if (inputBom == null)
63+
{
64+
Console.Error.WriteLine("Error reading input, you must specify a value for --input-file or pipe in content");
65+
return (int)ExitCode.IOError;
66+
}
67+
68+
var validated = false;
69+
70+
if (options.InputFormat.ToString().StartsWith("json"))
71+
{
72+
Console.WriteLine("Validating JSON SBOM...");
73+
validated = ValidateJson(options, inputBom);
74+
}
75+
else if (options.InputFormat.ToString().StartsWith("xml"))
76+
{
77+
Console.WriteLine("Validating XML SBOM...");
78+
validated = ValidateXml(options, inputBom);
79+
}
80+
81+
if (options.FailOnErrors && !validated)
82+
{
83+
return (int)ExitCode.OkFail;
84+
}
85+
86+
return (int)ExitCode.Ok;
87+
}
88+
89+
static void ValidateInputFormat(Options options)
90+
{
91+
if (options.InputFormat == InputFormat.autodetect && !string.IsNullOrEmpty(options.InputFile))
92+
{
93+
if (options.InputFile.EndsWith(".json"))
94+
{
95+
options.InputFormat = InputFormat.json;
96+
}
97+
else if (options.InputFile.EndsWith(".xml"))
98+
{
99+
options.InputFormat = InputFormat.xml;
100+
}
101+
}
102+
103+
if (options.InputFormat == InputFormat.json)
104+
{
105+
options.InputFormat = InputFormat.json_v1_2;
106+
}
107+
else if (options.InputFormat == InputFormat.xml)
108+
{
109+
options.InputFormat = InputFormat.xml_v1_2;
110+
}
111+
}
112+
113+
static string ReadInput(Options options)
114+
{
115+
string inputString = null;
116+
if (!string.IsNullOrEmpty(options.InputFile))
117+
{
118+
inputString = File.ReadAllText(options.InputFile);
119+
}
120+
else if (Console.IsInputRedirected)
121+
{
122+
var sb = new StringBuilder();
123+
string nextLine;
124+
do
125+
{
126+
nextLine = Console.ReadLine();
127+
sb.AppendLine(nextLine);
128+
} while (nextLine != null);
129+
inputString = sb.ToString();
130+
}
131+
return inputString;
132+
}
133+
134+
static bool ValidateXml(Options options, string sbomContents)
135+
{
136+
XmlReaderSettings settings = new XmlReaderSettings();
137+
var schemaVersion = options.InputFormat.ToString().Substring(5).Replace('_', '.');
138+
Console.WriteLine($"Using schema v{schemaVersion}");
139+
var schemaDirectory = Path.Join(Path.GetDirectoryName(Assembly.GetEntryAssembly().Location), "Schemas");
140+
settings.Schemas.Add(
141+
$"http://cyclonedx.org/schema/bom/{schemaVersion}",
142+
Path.Join(schemaDirectory, $"bom-{schemaVersion}.xsd"));
143+
settings.Schemas.Add(
144+
"http://cyclonedx.org/schema/spdx",
145+
Path.Join(schemaDirectory, "spdx.xsd"));
146+
settings.ValidationType = ValidationType.Schema;
147+
148+
var stream = new MemoryStream();
149+
var writer = new StreamWriter(stream);
150+
writer.Write(sbomContents);
151+
writer.Flush();
152+
stream.Position = 0;
153+
154+
XmlReader reader = XmlReader.Create(stream, settings);
155+
XmlDocument document = new XmlDocument();
156+
157+
try
158+
{
159+
document.Load(reader);
160+
161+
Console.WriteLine("SBOM successfully validated");
162+
return true;
163+
}
164+
catch (XmlSchemaValidationException exc)
165+
{
166+
var lineInfo = ((IXmlLineInfo)reader);
167+
if (lineInfo.HasLineInfo()) {
168+
Console.Error.WriteLine($"Validation failed at line number {lineInfo.LineNumber} and position {lineInfo.LinePosition}: {exc.Message}");
169+
}
170+
else
171+
{
172+
Console.Error.WriteLine($"Validation failed at position {stream.Position}: {exc.Message}");
173+
}
174+
return false;
175+
}
176+
}
177+
178+
static bool ValidateJson(Options options, string sbomContents)
179+
{
180+
var schemaVersion = options.InputFormat.ToString().Substring(6).Replace('_', '.');
181+
Console.WriteLine($"Using schema v{schemaVersion}");
182+
var schemaDirectory = Path.Join(Path.GetDirectoryName(Assembly.GetEntryAssembly().Location), "Schemas");
183+
var schemaFilename = Path.Join(schemaDirectory, $"bom-{schemaVersion}.schema.json");
184+
var spdxFilename = Path.Join(schemaDirectory, $"spdx.schema.json");
185+
var schemaContent = File.ReadAllText(schemaFilename);
186+
var spdxSchemaContent = File.ReadAllText(spdxFilename);
187+
188+
var schema = JsonSchema.FromText(schemaContent);
189+
var spdxSchema = JsonSchema.FromText(spdxSchemaContent);
190+
191+
SchemaRegistry.Global.Register(new Uri("file://spdx.schema.json"), spdxSchema);
192+
193+
var jsonDocument = JsonDocument.Parse(sbomContents);
194+
var validationOptions = new ValidationOptions
195+
{
196+
OutputFormat = OutputFormat.Detailed
197+
};
198+
199+
var result = schema.Validate(jsonDocument.RootElement, validationOptions);
200+
if (result.IsValid)
201+
{
202+
Console.WriteLine("SBOM successfully validated");
203+
return true;
204+
}
205+
else
206+
{
207+
Console.WriteLine(result.Message);
208+
Console.WriteLine(result.SchemaLocation);
209+
return false;
210+
}
211+
}
212+
}
213+
}

cyclonedx/ExitCode.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ namespace CycloneDX.CLI
1111
public enum ExitCode
1212
{
1313
Ok,
14+
OkFail,
15+
IOError,
1416
ParameterValidationError,
1517
UnsupportedFormat
1618
}

cyclonedx/Program.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ public static async Task<int> Main(string[] args)
3131
ConfigureConvertCommand(rootCommand);
3232
ConfigureDiffCommand(rootCommand);
3333
ConfigureMergeCommand(rootCommand);
34+
ValidateCommand.Configure(rootCommand);
3435

3536
return await rootCommand.InvokeAsync(args);
3637
}

0 commit comments

Comments
 (0)