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