| | 1 | | using System.Collections; |
| | 2 | | using System.Text.RegularExpressions; |
| | 3 | | using Spdx3.Exceptions; |
| | 4 | | using Spdx3.Model.Core.Classes; |
| | 5 | | using Spdx3.Model.Core.Enums; |
| | 6 | | using Spdx3.Model.SimpleLicensing.Classes; |
| | 7 | | using Spdx3.Model.Software.Classes; |
| | 8 | |
|
| | 9 | | namespace Spdx3.Model.Lite; |
| | 10 | |
|
| | 11 | | /// <summary> |
| | 12 | | /// Visitor that checks a catalog for compliance with the Spdx Lite domain requirements. |
| | 13 | | /// See https://spdx.github.io/spdx-spec/v3.0.1/annexes/spdx-lite/ |
| | 14 | | /// </summary> |
| | 15 | | internal class LiteDomainComplianceVisitor : ILiteDomainComplianceVisitor |
| | 16 | | { |
| 37 | 17 | | internal List<LiteDomainComplianceFinding> Findings { get; } = []; |
| | 18 | |
|
| | 19 | | public void Visit(SpdxDocument spdxDocument) |
| | 20 | | { |
| 3 | 21 | | CheckNotNullOrEmpty(LiteDomainComplianceFindingType.problem, spdxDocument, nameof(spdxDocument.CreationInfo), |
| 3 | 22 | | spdxDocument.CreationInfo); |
| 3 | 23 | | CheckNotNullOrEmpty(LiteDomainComplianceFindingType.problem, spdxDocument, nameof(spdxDocument.SpdxId), |
| 3 | 24 | | spdxDocument.SpdxId); |
| 3 | 25 | | CheckNotNullOrEmpty(LiteDomainComplianceFindingType.problem, spdxDocument, nameof(spdxDocument.Element), |
| 3 | 26 | | (IList)spdxDocument.Element); |
| 3 | 27 | | CheckNotNullOrEmpty(LiteDomainComplianceFindingType.problem, spdxDocument, nameof(spdxDocument.RootElement), |
| 3 | 28 | | (IList)spdxDocument.RootElement); |
| 3 | 29 | | CheckNotNullOrEmpty(LiteDomainComplianceFindingType.recommendation, spdxDocument, nameof(spdxDocument.Comment), |
| 3 | 30 | | spdxDocument.Comment); |
| 3 | 31 | | CheckNotNullOrEmpty(LiteDomainComplianceFindingType.recommendation, spdxDocument, |
| 3 | 32 | | nameof(spdxDocument.DataLicense), spdxDocument.DataLicense); |
| 3 | 33 | | CheckNotNullOrEmpty(LiteDomainComplianceFindingType.recommendation, spdxDocument, nameof(spdxDocument.Name), |
| 3 | 34 | | spdxDocument.Name); |
| 3 | 35 | | CheckNotNullOrEmpty(LiteDomainComplianceFindingType.recommendation, spdxDocument, |
| 3 | 36 | | nameof(spdxDocument.NamespaceMap), (IList)spdxDocument.NamespaceMap); |
| 3 | 37 | | CheckNotNullOrEmpty(LiteDomainComplianceFindingType.recommendation, spdxDocument, |
| 3 | 38 | | nameof(spdxDocument.VerifiedUsing), (IList)spdxDocument.VerifiedUsing); |
| 3 | 39 | | CheckAtLeastOneMemberOfType(LiteDomainComplianceFindingType.problem, spdxDocument, nameof(spdxDocument.Element), |
| 3 | 40 | | (IList)spdxDocument.Element, typeof(Sbom)); |
| 3 | 41 | | CheckContainsOnlyType(LiteDomainComplianceFindingType.recommendation, spdxDocument, |
| 3 | 42 | | nameof(spdxDocument.Element), (IList)spdxDocument.Element, typeof(Sbom)); |
| 3 | 43 | | } |
| | 44 | |
|
| | 45 | | public void Visit(CreationInfo creationInfo) |
| | 46 | | { |
| 3 | 47 | | CheckNotNullOrEmpty(LiteDomainComplianceFindingType.problem, creationInfo, nameof(creationInfo.Created), |
| 3 | 48 | | creationInfo.Created); |
| 3 | 49 | | CheckMatchesPattern(LiteDomainComplianceFindingType.problem, creationInfo, nameof(creationInfo.SpecVersion), |
| 3 | 50 | | creationInfo.SpecVersion, @"^3\.0\.\d+$"); |
| 3 | 51 | | CheckNotNullOrEmpty(LiteDomainComplianceFindingType.problem, creationInfo, nameof(creationInfo.CreatedBy), |
| 3 | 52 | | (IList)creationInfo.CreatedBy); |
| 3 | 53 | | CheckNotNullOrEmpty(LiteDomainComplianceFindingType.recommendation, creationInfo, nameof(creationInfo.Comment), |
| 3 | 54 | | creationInfo.Comment); |
| 3 | 55 | | } |
| | 56 | |
|
| | 57 | | public void Visit(Agent agent) |
| | 58 | | { |
| 5 | 59 | | CheckNotNullOrEmpty(LiteDomainComplianceFindingType.problem, agent, nameof(agent.CreationInfo), |
| 5 | 60 | | agent.CreationInfo); |
| 5 | 61 | | CheckNotNullOrEmpty(LiteDomainComplianceFindingType.problem, agent, nameof(agent.SpdxId), agent.SpdxId); |
| 5 | 62 | | CheckNotNullOrEmpty(LiteDomainComplianceFindingType.problem, agent, nameof(agent.Name), agent.Name); |
| 5 | 63 | | CheckNotNullOrEmpty(LiteDomainComplianceFindingType.recommendation, agent, nameof(agent.ExternalIdentifier), |
| 5 | 64 | | (IList)agent.ExternalIdentifier); |
| 5 | 65 | | } |
| | 66 | |
|
| | 67 | | public void Visit(ExternalIdentifier externalIdentifier) |
| | 68 | | { |
| 2 | 69 | | CheckNotNullOrEmpty(LiteDomainComplianceFindingType.problem, externalIdentifier, |
| 2 | 70 | | nameof(externalIdentifier.ExternalIdentifierType), externalIdentifier.ExternalIdentifierType); |
| 2 | 71 | | CheckNotNullOrEmpty(LiteDomainComplianceFindingType.problem, externalIdentifier, |
| 2 | 72 | | nameof(externalIdentifier.Identifier), externalIdentifier.Identifier); |
| 2 | 73 | | } |
| | 74 | |
|
| | 75 | | public void Visit(Hash hash) |
| | 76 | | { |
| 3 | 77 | | CheckNotNullOrEmpty(LiteDomainComplianceFindingType.problem, hash, nameof(hash.Algorithm), hash.Algorithm); |
| 3 | 78 | | CheckNotNullOrEmpty(LiteDomainComplianceFindingType.problem, hash, nameof(hash.HashValue), hash.HashValue); |
| 3 | 79 | | CheckNotNullOrEmpty(LiteDomainComplianceFindingType.recommendation, hash, nameof(hash.Comment), hash.Comment); |
| 3 | 80 | | } |
| | 81 | |
|
| | 82 | | public void Visit(NamespaceMap namespaceMap) |
| | 83 | | { |
| 1 | 84 | | CheckNotNullOrEmpty(LiteDomainComplianceFindingType.problem, namespaceMap, nameof(namespaceMap.Prefix), |
| 1 | 85 | | namespaceMap.Prefix); |
| 1 | 86 | | CheckNotNullOrEmpty(LiteDomainComplianceFindingType.problem, namespaceMap, nameof(namespaceMap.Namespace), |
| 1 | 87 | | namespaceMap.Namespace); |
| 1 | 88 | | } |
| | 89 | |
|
| | 90 | | public void Visit(Relationship relationship) |
| | 91 | | { |
| 4 | 92 | | CheckNotNullOrEmpty(LiteDomainComplianceFindingType.problem, relationship, nameof(relationship.CreationInfo), |
| 4 | 93 | | relationship.CreationInfo); |
| 4 | 94 | | CheckNotNullOrEmpty(LiteDomainComplianceFindingType.problem, relationship, nameof(relationship.From), |
| 4 | 95 | | relationship.From); |
| 4 | 96 | | CheckNotNullOrEmpty(LiteDomainComplianceFindingType.problem, relationship, |
| 4 | 97 | | nameof(relationship.RelationshipType), relationship.RelationshipType); |
| 4 | 98 | | CheckNotNullOrEmpty(LiteDomainComplianceFindingType.problem, relationship, nameof(relationship.SpdxId), |
| 4 | 99 | | relationship.SpdxId); |
| 4 | 100 | | CheckNotNullOrEmpty(LiteDomainComplianceFindingType.problem, relationship, nameof(relationship.To), |
| 4 | 101 | | relationship.To); |
| 4 | 102 | | } |
| | 103 | |
|
| | 104 | | public void Visit(LicenseExpression licenseExpression) |
| | 105 | | { |
| 3 | 106 | | CheckNotNullOrEmpty(LiteDomainComplianceFindingType.problem, licenseExpression, |
| 3 | 107 | | nameof(licenseExpression.CreationInfo), licenseExpression.CreationInfo); |
| 3 | 108 | | CheckNotNullOrEmpty(LiteDomainComplianceFindingType.problem, licenseExpression, |
| 3 | 109 | | nameof(licenseExpression.LicenseExpressionText), licenseExpression.LicenseExpressionText); |
| 3 | 110 | | CheckNotNullOrEmpty(LiteDomainComplianceFindingType.problem, licenseExpression, |
| 3 | 111 | | nameof(licenseExpression.SpdxId), licenseExpression.SpdxId); |
| 3 | 112 | | CheckNotNullOrEmpty(LiteDomainComplianceFindingType.recommendation, licenseExpression, |
| 3 | 113 | | nameof(licenseExpression.LicenseListVersion), licenseExpression.LicenseListVersion); |
| 3 | 114 | | } |
| | 115 | |
|
| | 116 | | public void Visit(SimpleLicensingText simpleLicensingText) |
| | 117 | | { |
| 0 | 118 | | CheckNotNullOrEmpty(LiteDomainComplianceFindingType.problem, simpleLicensingText, |
| 0 | 119 | | nameof(simpleLicensingText.CreationInfo), simpleLicensingText.CreationInfo); |
| 0 | 120 | | CheckNotNullOrEmpty(LiteDomainComplianceFindingType.problem, simpleLicensingText, |
| 0 | 121 | | nameof(simpleLicensingText.LicenseText), simpleLicensingText.LicenseText); |
| 0 | 122 | | CheckNotNullOrEmpty(LiteDomainComplianceFindingType.problem, simpleLicensingText, |
| 0 | 123 | | nameof(simpleLicensingText.SpdxId), simpleLicensingText.SpdxId); |
| 0 | 124 | | CheckNotNullOrEmpty(LiteDomainComplianceFindingType.recommendation, simpleLicensingText, |
| 0 | 125 | | nameof(simpleLicensingText.Comment), simpleLicensingText.Comment); |
| 0 | 126 | | } |
| | 127 | |
|
| | 128 | | public void Visit(Package package) |
| | 129 | | { |
| 2 | 130 | | CheckNotNullOrEmpty(LiteDomainComplianceFindingType.problem, package, nameof(package.CopyrightText), |
| 2 | 131 | | package.CopyrightText); |
| 2 | 132 | | CheckNotNullOrEmpty(LiteDomainComplianceFindingType.problem, package, nameof(package.CreationInfo), |
| 2 | 133 | | package.CreationInfo); |
| 2 | 134 | | CheckNotNullOrEmpty(LiteDomainComplianceFindingType.problem, package, nameof(package.Name), package.Name); |
| 2 | 135 | | CheckNotNullOrEmpty(LiteDomainComplianceFindingType.problem, package, nameof(package.PackageVersion), |
| 2 | 136 | | package.PackageVersion); |
| 2 | 137 | | CheckNotNullOrEmpty(LiteDomainComplianceFindingType.problem, package, nameof(package.SpdxId), package.SpdxId); |
| 2 | 138 | | CheckNotNullOrEmpty(LiteDomainComplianceFindingType.problem, package, nameof(package.SuppliedBy), |
| 2 | 139 | | package.SuppliedBy); |
| | 140 | |
|
| 2 | 141 | | CheckNotNullOrEmpty(LiteDomainComplianceFindingType.recommendation, package, nameof(package.AttributionText), |
| 2 | 142 | | (IList)package.AttributionText); |
| 2 | 143 | | CheckNotNullOrEmpty(LiteDomainComplianceFindingType.recommendation, package, nameof(package.BuiltTime), |
| 2 | 144 | | package.BuiltTime); |
| 2 | 145 | | CheckNotNullOrEmpty(LiteDomainComplianceFindingType.recommendation, package, nameof(package.Comment), |
| 2 | 146 | | package.Comment); |
| 2 | 147 | | CheckNotNullOrEmpty(LiteDomainComplianceFindingType.recommendation, package, nameof(package.DownloadLocation), |
| 2 | 148 | | package.DownloadLocation); |
| 2 | 149 | | CheckNotNullOrEmpty(LiteDomainComplianceFindingType.recommendation, package, nameof(package.HomePage), |
| 2 | 150 | | package.HomePage); |
| 2 | 151 | | CheckNotNullOrEmpty(LiteDomainComplianceFindingType.recommendation, package, nameof(package.ReleaseTime), |
| 2 | 152 | | package.ReleaseTime); |
| 2 | 153 | | CheckNotNullOrEmpty(LiteDomainComplianceFindingType.recommendation, package, nameof(package.SupportLevel), |
| 2 | 154 | | (IList)package.SupportLevel); |
| 2 | 155 | | CheckNotNullOrEmpty(LiteDomainComplianceFindingType.recommendation, package, nameof(package.ValidUntilTime), |
| 2 | 156 | | package.ValidUntilTime); |
| 2 | 157 | | CheckNotNullOrEmpty(LiteDomainComplianceFindingType.recommendation, package, nameof(package.VerifiedUsing), |
| 2 | 158 | | (IList)package.VerifiedUsing); |
| 2 | 159 | | CheckContainsOnlyType(LiteDomainComplianceFindingType.recommendation, package, nameof(package.VerifiedUsing), |
| 2 | 160 | | (IList)package.VerifiedUsing, typeof(Hash)); |
| | 161 | |
|
| 2 | 162 | | if (string.IsNullOrWhiteSpace(package.DownloadLocation) && |
| 2 | 163 | | (package.PackageUrl is null || string.IsNullOrWhiteSpace(package.PackageUrl.ToString()))) |
| | 164 | | { |
| 0 | 165 | | Findings.Add(new LiteDomainComplianceFinding(LiteDomainComplianceFindingType.problem, package, |
| 0 | 166 | | $"Either {nameof(package.DownloadLocation)} or {nameof(package.PackageUrl)} is required")); |
| | 167 | | } |
| | 168 | |
|
| 2 | 169 | | if (package.Catalog is null) |
| | 170 | | { |
| 0 | 171 | | throw new Spdx3Exception($"Cannot find catalog from Package '{package.SpdxId}'"); |
| | 172 | | } |
| | 173 | |
|
| 2 | 174 | | MustHaveExactlyOneRelationshipOfType(package, RelationshipType.hasDeclaredLicense); |
| 2 | 175 | | MustHaveExactlyOneRelationshipOfType(package, RelationshipType.hasConcludedLicense); |
| 2 | 176 | | } |
| | 177 | |
|
| | 178 | | private void MustHaveExactlyOneRelationshipOfType(Package package, RelationshipType relType) |
| | 179 | | { |
| 4 | 180 | | var relationshipsOfType = package.Catalog.GetRelationshipsOfType(relType); |
| | 181 | |
|
| 4 | 182 | | if (relationshipsOfType.Count() != 1) |
| | 183 | | { |
| 0 | 184 | | Findings.Add(new LiteDomainComplianceFinding(LiteDomainComplianceFindingType.problem, package, |
| 0 | 185 | | $"Must have exactly one relationship from this package of type '{relType}'")); |
| | 186 | | } |
| | 187 | | else |
| | 188 | | { |
| 4 | 189 | | if (relationshipsOfType.First().To.Count() != 1) |
| | 190 | | { |
| 0 | 191 | | Findings.Add(new LiteDomainComplianceFinding(LiteDomainComplianceFindingType.problem, package, |
| 0 | 192 | | $"{nameof(Relationship)} from {nameof(Package)} of type '{relType}' " + |
| 0 | 193 | | $"must point to exactly one {nameof(AnyLicenseInfo)} object")); |
| | 194 | | } |
| | 195 | | else |
| | 196 | | { |
| 4 | 197 | | if (!relationshipsOfType.First().To.First().GetType().IsAssignableTo(typeof(AnyLicenseInfo))) |
| | 198 | | { |
| 0 | 199 | | Findings.Add(new LiteDomainComplianceFinding(LiteDomainComplianceFindingType.problem, package, |
| 0 | 200 | | $"{nameof(Relationship)} from {nameof(Package)} of type '{RelationshipType.hasConcludedLicense}' |
| 0 | 201 | | $"is not pointing to an {nameof(AnyLicenseInfo)} object")); |
| | 202 | | } |
| | 203 | | } |
| | 204 | | } |
| 4 | 205 | | } |
| | 206 | |
|
| | 207 | | public void Visit(Sbom sbom) |
| | 208 | | { |
| 2 | 209 | | CheckNotNullOrEmpty(LiteDomainComplianceFindingType.problem, sbom, nameof(sbom.CreationInfo), |
| 2 | 210 | | sbom.CreationInfo); |
| 2 | 211 | | CheckNotNullOrEmpty(LiteDomainComplianceFindingType.problem, sbom, nameof(sbom.SpdxId), sbom.SpdxId); |
| 2 | 212 | | CheckNotNullOrEmpty(LiteDomainComplianceFindingType.problem, sbom, nameof(sbom.Element), (IList)sbom.Element); |
| 2 | 213 | | CheckNotNullOrEmpty(LiteDomainComplianceFindingType.problem, sbom, nameof(sbom.RootElement), |
| 2 | 214 | | (IList)sbom.RootElement); |
| 2 | 215 | | CheckNotNullOrEmpty(LiteDomainComplianceFindingType.recommendation, sbom, nameof(sbom.Comment), sbom.Comment); |
| 2 | 216 | | CheckNotNullOrEmpty(LiteDomainComplianceFindingType.recommendation, sbom, nameof(sbom.Name), sbom.Name); |
| 2 | 217 | | CheckNotNullOrEmpty(LiteDomainComplianceFindingType.recommendation, sbom, nameof(sbom.VerifiedUsing), |
| 2 | 218 | | (IList)sbom.VerifiedUsing); |
| 2 | 219 | | CheckAtLeastOneMemberOfType(LiteDomainComplianceFindingType.problem, sbom, nameof(sbom.Element), |
| 2 | 220 | | (IList)sbom.Element, typeof(Package)); |
| 2 | 221 | | CheckContainsOnlyType(LiteDomainComplianceFindingType.recommendation, sbom, nameof(sbom.Element), |
| 2 | 222 | | (IList)sbom.Element, typeof(Package)); |
| 2 | 223 | | } |
| | 224 | |
|
| | 225 | | private void CheckNotNullOrEmpty(LiteDomainComplianceFindingType findingType, BaseModelClass obj, |
| | 226 | | string propertyName, IList? listVal) |
| | 227 | | { |
| 32 | 228 | | var verb = findingType == LiteDomainComplianceFindingType.problem ? "requires" : "should have"; |
| | 229 | |
|
| 32 | 230 | | if (listVal is null || listVal.Count < 1) |
| | 231 | | { |
| 13 | 232 | | Findings.Add(new LiteDomainComplianceFinding(findingType, obj, propertyName, |
| 13 | 233 | | $"List {verb} at least one item")); |
| | 234 | | } |
| 32 | 235 | | } |
| | 236 | |
|
| | 237 | |
|
| | 238 | | private void CheckAtLeastOneMemberOfType(LiteDomainComplianceFindingType findingType, BaseModelClass obj, |
| | 239 | | string propertyName, IList? listVal, Type requiredType) |
| | 240 | | { |
| 5 | 241 | | var verb = findingType == LiteDomainComplianceFindingType.problem ? "requires" : "should have"; |
| | 242 | |
|
| 5 | 243 | | if (listVal is null || listVal.Count < 1) |
| | 244 | | { |
| 1 | 245 | | Findings.Add(new LiteDomainComplianceFinding(findingType, obj, propertyName, |
| 1 | 246 | | $"List {verb} at least one item of type {requiredType.Name}")); |
| 1 | 247 | | return; |
| | 248 | | } |
| | 249 | |
|
| 12 | 250 | | foreach (var member in listVal) |
| | 251 | | { |
| 4 | 252 | | if (member.GetType() == requiredType) |
| | 253 | | { |
| 4 | 254 | | return; |
| | 255 | | } |
| | 256 | | } |
| | 257 | |
|
| 0 | 258 | | Findings.Add(new LiteDomainComplianceFinding(findingType, obj, propertyName, |
| 0 | 259 | | $"List {verb} at least one item of type {requiredType.Name}")); |
| 4 | 260 | | } |
| | 261 | |
|
| | 262 | | private void CheckContainsOnlyType(LiteDomainComplianceFindingType findingType, BaseModelClass obj, |
| | 263 | | string propertyName, IList? listVal, Type desiredType) |
| | 264 | | { |
| 7 | 265 | | var verb = findingType == LiteDomainComplianceFindingType.problem ? "must" : "should"; |
| | 266 | |
|
| 7 | 267 | | if (listVal is null) |
| | 268 | | { |
| 0 | 269 | | return; |
| | 270 | | } |
| | 271 | |
|
| 24 | 272 | | foreach (var member in listVal) |
| | 273 | | { |
| 5 | 274 | | if (member.GetType() != desiredType) |
| | 275 | | { |
| 0 | 276 | | Findings.Add(new LiteDomainComplianceFinding(findingType, obj, propertyName, |
| 0 | 277 | | $"List {verb} contain only items of type {desiredType.Name}")); |
| 0 | 278 | | return; |
| | 279 | | } |
| | 280 | | } |
| 7 | 281 | | } |
| | 282 | |
|
| | 283 | | private void CheckMatchesPattern(LiteDomainComplianceFindingType findingType, BaseModelClass obj, |
| | 284 | | string propertyName, string? strVal, string regexPattern) |
| | 285 | | { |
| 3 | 286 | | if (strVal is not null && !Regex.Match(strVal, regexPattern).Success) |
| | 287 | | { |
| 0 | 288 | | Findings.Add( |
| 0 | 289 | | new LiteDomainComplianceFinding(findingType, obj, propertyName, $"Value '{strVal}' is invalid")); |
| | 290 | | } |
| 3 | 291 | | } |
| | 292 | |
|
| | 293 | | private void CheckNotNullOrEmpty(LiteDomainComplianceFindingType findingType, BaseModelClass obj, |
| | 294 | | string propertyName, object? objVal) |
| | 295 | | { |
| 93 | 296 | | var verb = findingType == LiteDomainComplianceFindingType.problem ? "required" : "recommended"; |
| | 297 | |
|
| 93 | 298 | | if (objVal is null) |
| | 299 | | { |
| 16 | 300 | | Findings.Add(new LiteDomainComplianceFinding(findingType, obj, propertyName, $"Value {verb}")); |
| | 301 | | } |
| 93 | 302 | | } |
| | 303 | |
|
| | 304 | | private void CheckNotNullOrEmpty(LiteDomainComplianceFindingType findingType, BaseModelClass obj, string fieldName, |
| | 305 | | Uri? uriVal) |
| | 306 | | { |
| 22 | 307 | | if (uriVal is null) |
| | 308 | | { |
| 1 | 309 | | var verb = findingType == LiteDomainComplianceFindingType.problem ? "required" : "recommended"; |
| 1 | 310 | | Findings.Add(new LiteDomainComplianceFinding(findingType, obj, fieldName, $"Value {verb}")); |
| 1 | 311 | | return; |
| | 312 | | } |
| | 313 | |
|
| 21 | 314 | | if (!uriVal.IsWellFormedOriginalString()) |
| | 315 | | { |
| 0 | 316 | | Findings.Add(new LiteDomainComplianceFinding(findingType, obj, fieldName, |
| 0 | 317 | | $"Value of '{uriVal.ToString()}' is not a well-formed URI")); |
| | 318 | | } |
| 21 | 319 | | } |
| | 320 | | } |