1# Copyright (c) 2020, 2021 The Linux Foundation 2# 3# SPDX-License-Identifier: Apache-2.0 4 5import re 6from datetime import datetime 7 8from west import log 9 10from zspdx.util import getHashes 11from zspdx.version import SPDX_VERSION_2_3 12 13CPE23TYPE_REGEX = ( 14 r'^cpe:2\.3:[aho\*\-](:(((\?*|\*?)([a-zA-Z0-9\-\._]|(\\[\\\*\?!"#$$%&\'\(\)\+,\/:;<=>@\[\]\^' 15 r"`\{\|}~]))+(\?*|\*?))|[\*\-])){5}(:(([a-zA-Z]{2,3}(-([a-zA-Z]{2}|[0-9]{3}))?)|[\*\-]))(:(((\?*" 16 r'|\*?)([a-zA-Z0-9\-\._]|(\\[\\\*\?!"#$$%&\'\(\)\+,\/:;<=>@\[\]\^`\{\|}~]))+(\?*|\*?))|[\*\-])){4}$' 17) 18PURL_REGEX = r"^pkg:.+(\/.+)?\/.+(@.+)?(\?.+)?(#.+)?$" 19 20def _normalize_spdx_name(name): 21 # Replace "_" by "-" since it's not allowed in spdx ID 22 return name.replace("_", "-") 23 24# Output tag-value SPDX 2.3 content for the given Relationship object. 25# Arguments: 26# 1) f: file handle for SPDX document 27# 2) rln: Relationship object being described 28def writeRelationshipSPDX(f, rln): 29 f.write( 30 f"Relationship: {_normalize_spdx_name(rln.refA)} {rln.rlnType} " 31 f"{_normalize_spdx_name(rln.refB)}\n" 32 ) 33 34# Output tag-value SPDX 2.3 content for the given File object. 35# Arguments: 36# 1) f: file handle for SPDX document 37# 2) bf: File object being described 38def writeFileSPDX(f, bf): 39 spdx_normalize_spdx_id = _normalize_spdx_name(bf.spdxID) 40 41 f.write(f"""FileName: ./{bf.relpath} 42SPDXID: {spdx_normalize_spdx_id} 43FileChecksum: SHA1: {bf.sha1} 44""") 45 if bf.sha256 != "": 46 f.write(f"FileChecksum: SHA256: {bf.sha256}\n") 47 if bf.md5 != "": 48 f.write(f"FileChecksum: MD5: {bf.md5}\n") 49 f.write(f"LicenseConcluded: {bf.concludedLicense}\n") 50 if len(bf.licenseInfoInFile) == 0: 51 f.write("LicenseInfoInFile: NONE\n") 52 else: 53 for licInfoInFile in bf.licenseInfoInFile: 54 f.write(f"LicenseInfoInFile: {licInfoInFile}\n") 55 f.write(f"FileCopyrightText: {bf.copyrightText}\n\n") 56 57 # write file relationships 58 if len(bf.rlns) > 0: 59 for rln in bf.rlns: 60 writeRelationshipSPDX(f, rln) 61 f.write("\n") 62 63def generateDowloadUrl(url, revision): 64 # Only git is supported 65 # walker.py only parse revision if it's from git repositiory 66 if len(revision) == 0: 67 return url 68 69 return f'git+{url}@{revision}' 70 71# Output tag-value SPDX content for the given Package object. 72# Arguments: 73# 1) f: file handle for SPDX document 74# 2) pkg: Package object being described 75# 3) spdx_version: SPDX specification version 76def writePackageSPDX(f, pkg, spdx_version=SPDX_VERSION_2_3): 77 spdx_normalized_name = _normalize_spdx_name(pkg.cfg.name) 78 spdx_normalize_spdx_id = _normalize_spdx_name(pkg.cfg.spdxID) 79 80 f.write(f"""##### Package: {spdx_normalized_name} 81 82PackageName: {spdx_normalized_name} 83SPDXID: {spdx_normalize_spdx_id} 84PackageLicenseConcluded: {pkg.concludedLicense} 85""") 86 f.write(f"""PackageLicenseDeclared: {pkg.cfg.declaredLicense} 87PackageCopyrightText: {pkg.cfg.copyrightText} 88""") 89 90 # PrimaryPackagePurpose is only available in SPDX 2.3 and later 91 if spdx_version >= SPDX_VERSION_2_3 and pkg.cfg.primaryPurpose != "": 92 f.write(f"PrimaryPackagePurpose: {pkg.cfg.primaryPurpose}\n") 93 94 if len(pkg.cfg.url) > 0: 95 downloadUrl = generateDowloadUrl(pkg.cfg.url, pkg.cfg.revision) 96 f.write(f"PackageDownloadLocation: {downloadUrl}\n") 97 else: 98 f.write("PackageDownloadLocation: NOASSERTION\n") 99 100 if len(pkg.cfg.version) > 0: 101 f.write(f"PackageVersion: {pkg.cfg.version}\n") 102 elif len(pkg.cfg.revision) > 0: 103 f.write(f"PackageVersion: {pkg.cfg.revision}\n") 104 105 for ref in pkg.cfg.externalReferences: 106 if re.fullmatch(CPE23TYPE_REGEX, ref): 107 f.write(f"ExternalRef: SECURITY cpe23Type {ref}\n") 108 elif re.fullmatch(PURL_REGEX, ref): 109 f.write(f"ExternalRef: PACKAGE_MANAGER purl {ref}\n") 110 else: 111 log.wrn(f"Unknown external reference ({ref})") 112 113 # flag whether files analyzed / any files present 114 if len(pkg.files) > 0: 115 if len(pkg.licenseInfoFromFiles) > 0: 116 for licFromFiles in pkg.licenseInfoFromFiles: 117 f.write(f"PackageLicenseInfoFromFiles: {licFromFiles}\n") 118 else: 119 f.write("PackageLicenseInfoFromFiles: NOASSERTION\n") 120 f.write(f"FilesAnalyzed: true\nPackageVerificationCode: {pkg.verificationCode}\n\n") 121 else: 122 f.write("FilesAnalyzed: false\nPackageComment: Utility target; no files\n\n") 123 124 # write package relationships 125 if len(pkg.rlns) > 0: 126 for rln in pkg.rlns: 127 writeRelationshipSPDX(f, rln) 128 f.write("\n") 129 130 # write package files, if any 131 if len(pkg.files) > 0: 132 bfs = list(pkg.files.values()) 133 bfs.sort(key = lambda x: x.relpath) 134 for bf in bfs: 135 writeFileSPDX(f, bf) 136 137# Output tag-value SPDX 2.3 content for a custom license. 138# Arguments: 139# 1) f: file handle for SPDX document 140# 2) lic: custom license ID being described 141def writeOtherLicenseSPDX(f, lic): 142 f.write(f"""LicenseID: {lic} 143ExtractedText: {lic} 144LicenseName: {lic} 145LicenseComment: Corresponds to the license ID `{lic}` detected in an SPDX-License-Identifier: tag. 146""") 147 148# Output tag-value SPDX content for the given Document object. 149# Arguments: 150# 1) f: file handle for SPDX document 151# 2) doc: Document object being described 152# 3) spdx_version: SPDX specification version 153def writeDocumentSPDX(f, doc, spdx_version=SPDX_VERSION_2_3): 154 spdx_normalized_name = _normalize_spdx_name(doc.cfg.name) 155 156 f.write(f"""SPDXVersion: SPDX-{spdx_version} 157DataLicense: CC0-1.0 158SPDXID: SPDXRef-DOCUMENT 159DocumentName: {spdx_normalized_name} 160DocumentNamespace: {doc.cfg.namespace} 161Creator: Tool: Zephyr SPDX builder 162Created: {datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%SZ")} 163 164""") 165 166 # write any external document references 167 if len(doc.externalDocuments) > 0: 168 extDocs = list(doc.externalDocuments) 169 extDocs.sort(key = lambda x: x.cfg.docRefID) 170 for extDoc in extDocs: 171 f.write( 172 f"ExternalDocumentRef: {extDoc.cfg.docRefID} {extDoc.cfg.namespace} " 173 f"SHA1: {extDoc.myDocSHA1}\n" 174 ) 175 f.write("\n") 176 177 # write relationships owned by this Document (not by its Packages, etc.), if any 178 if len(doc.relationships) > 0: 179 for rln in doc.relationships: 180 writeRelationshipSPDX(f, rln) 181 f.write("\n") 182 183 # write packages 184 for pkg in doc.pkgs.values(): 185 writePackageSPDX(f, pkg, spdx_version) 186 187 # write other license info, if any 188 if len(doc.customLicenseIDs) > 0: 189 for lic in sorted(list(doc.customLicenseIDs)): 190 writeOtherLicenseSPDX(f, lic) 191 192# Open SPDX document file for writing, write the document, and calculate 193# its hash for other referring documents to use. 194# Arguments: 195# 1) spdxPath: path to write SPDX document 196# 2) doc: SPDX Document object to write 197# 3) spdx_version: SPDX specification version 198def writeSPDX(spdxPath, doc, spdx_version=SPDX_VERSION_2_3): 199 # create and write document to disk 200 try: 201 log.inf(f"Writing SPDX {spdx_version} document {doc.cfg.name} to {spdxPath}") 202 with open(spdxPath, "w") as f: 203 writeDocumentSPDX(f, doc, spdx_version) 204 except OSError as e: 205 log.err(f"Error: Unable to write to {spdxPath}: {str(e)}") 206 return False 207 208 # calculate hash of the document we just wrote 209 hashes = getHashes(spdxPath) 210 if not hashes: 211 log.err("Error: created document but unable to calculate hash values") 212 return False 213 doc.myDocSHA1 = hashes[0] 214 215 return True 216