FOSSology  4.7.1
Open Source License Compliance by Open Source Software
cyclonedx.php
Go to the documentation of this file.
1 <?php
2 /*
3  SPDX-FileCopyrightText: © 2023 Sushant Kumar(sushantmishra02102002@gmail.com)
4 
5  SPDX-License-Identifier: GPL-2.0-only
6 */
20 namespace Fossology\CycloneDX;
21 
34 
35 include_once(__DIR__ . "/version.php");
36 include_once(__DIR__ . "/reportgenerator.php");
37 
42 class CycloneDXAgent extends Agent
43 {
44  const OUTPUT_FORMAT_KEY = "outputFormat";
45  const DEFAULT_OUTPUT_FORMAT = "cyclonedx_json";
46  const UPLOADS_ADD_KEY = "uploadsAdd";
47 
51  private $additionalUploads = [];
52 
61  private $reportutils;
65  private $uploadDao;
69  private $clearingDao;
74  private $licenseDao;
78  protected $dbManager;
82  private $licenseMap;
86  protected $agentNames = AgentRef::AGENT_LIST;
90  protected $uri;
95  private $licensesInDocument = [];
103  private $packageName;
104 
105  function __construct()
106  {
107  // deduce the agent name from the command line arguments
108  $args = getopt("", array(
109  self::OUTPUT_FORMAT_KEY.'::',
110  self::UPLOADS_ADD_KEY.'::'
111  ));
112  $agentName = "";
113  if (array_key_exists(self::OUTPUT_FORMAT_KEY, $args)) {
114  $agentName = trim($args[self::OUTPUT_FORMAT_KEY]);
115  }
116  if (empty($agentName)) {
118  }
119  if (array_key_exists(self::UPLOADS_ADD_KEY, $args)) {
120  $uploadsString = $args[self::UPLOADS_ADD_KEY];
121  if (!empty($uploadsString)) {
122  $this->additionalUploads = explode(',', $uploadsString);
123  }
124  }
125 
126  parent::__construct($agentName, AGENT_VERSION, AGENT_REV);
127 
128  $this->uploadDao = $this->container->get('dao.upload');
129  $this->clearingDao = $this->container->get('dao.clearing');
130  $this->licenseDao = $this->container->get('dao.license');
131  $this->dbManager = $this->container->get('db.manager');
132 
133  $this->reportutils = new ReportUtils();
134  $this->reportGenerator = new BomReportGenerator();
135  }
136 
141  function processUploadId($uploadId)
142  {
143  $this->licenseMap = new LicenseMap($this->dbManager, $this->groupId, LicenseMap::REPORT, true);
144 
145  $packageNodes = $this->renderPackage($uploadId);
146 
147  $this->computeUri($uploadId);
148 
149  $this->writeReport($packageNodes, $uploadId);
150  return true;
151  }
152 
158  protected function getUri($fileBase)
159  {
160  if (count($this->additionalUploads) > 0) {
161  $fileName = $fileBase . "multifile" . "_" . strtoupper($this->outputFormat);
162  } else {
163  $fileName = $fileBase. strtoupper($this->outputFormat)."_".$this->packageName;
164  }
165 
166  return $fileName .".json" ;
167  }
168 
173  protected function computeUri($uploadId)
174  {
175  global $SysConf;
176  $upload = $this->uploadDao->getUpload($uploadId);
177  $this->packageName = $upload->getFilename();
178 
179  $fileBase = $SysConf['FOSSOLOGY']['path']."/report/";
180 
181  $this->uri = $this->getUri($fileBase);
182  }
183 
189  protected function renderPackage($uploadId)
190  {
191  global $SysConf;
192  $uploadTreeTableName = $this->uploadDao->getUploadtreeTableName($uploadId);
193  $itemTreeBounds = $this->uploadDao->getParentItemBounds($uploadId, $uploadTreeTableName);
194  $this->heartbeat(0);
195 
196  $filesWithLicenses = $this->reportutils
197  ->getFilesWithLicensesFromClearings($itemTreeBounds, $this->groupId,
198  $this, $this->licensesInDocument);
199  $this->heartbeat(0);
200 
201  $this->reportutils->addClearingStatus($filesWithLicenses, $itemTreeBounds, $this->groupId);
202  $this->heartbeat(0);
203 
204  $this->reportutils->addScannerResults($filesWithLicenses, $itemTreeBounds, $this->groupId, $this->licensesInDocument);
205  $this->heartbeat(0);
206 
207  $this->reportutils->addCopyrightResults($filesWithLicenses, $uploadId);
208  $this->heartbeat(0);
209 
210  $customLicenseTexts = $this->clearingDao->getMainLicenseReportInfos($uploadId, $this->groupId);
211 
212  $upload = $this->uploadDao->getUpload($uploadId);
213  $components = $this->generateFileComponents($filesWithLicenses, $upload->getTreeTableName(), $uploadId, $itemTreeBounds, $customLicenseTexts);
214 
215  $mainLicenseIds = $this->clearingDao->getMainLicenseIds($uploadId, $this->groupId);
216  $mainLicenses = array();
217  $seenLicenseIds = array();
218  foreach ($mainLicenseIds as $licId) {
219  $reportedLicenseId = $this->licenseMap->getProjectedId($licId);
220  $mainLicObj = $this->licenseDao->getLicenseById($reportedLicenseId, $this->groupId);
221  if ($mainLicObj === null) {
222  continue;
223  }
224 
225  $licensedata = $this->getLicenseDataForCycloneDX($mainLicObj, $licId, $customLicenseTexts);
226  $mainLicenses[] = $this->reportGenerator->createLicense($licensedata);
227 
228  $customText = array_key_exists($licId, $customLicenseTexts) ? $customLicenseTexts[$licId] : null;
229  $licText = !empty($customText) ? $customText : $mainLicObj->getText();
230  $reportLicId = $mainLicObj->getId() . "-" . md5($licText);
231  $seenLicenseIds[$reportLicId] = true;
232  }
233 
234  foreach ($filesWithLicenses as $fileNode) {
235  $licenseIds = !empty($fileNode->getConcludedLicenses())
236  ? $fileNode->getConcludedLicenses()
237  : $fileNode->getScanners();
238  foreach ($licenseIds as $licenseId) {
239  if (array_key_exists($licenseId, $this->licensesInDocument) && !array_key_exists($licenseId, $seenLicenseIds)) {
240  $seenLicenseIds[$licenseId] = true;
241  $licObj = $this->licensesInDocument[$licenseId]->getLicenseObj();
242  $isCustomText = $this->licensesInDocument[$licenseId]->isCustomText();
243  $licensedata = $this->getLicenseDataForCycloneDX($licObj, $licenseId, $customLicenseTexts, $isCustomText);
244  $mainLicenses[] = $this->reportGenerator->createLicense($licensedata);
245  }
246  }
247  }
248 
249  $hashes = $this->uploadDao->getUploadHashes($uploadId);
250  $serializedhash = array();
251  $serializedhash[] = $this->reportGenerator->createHash('SHA-1', $hashes['sha1']);
252  $serializedhash[] = $this->reportGenerator->createHash('MD5', $hashes['md5']);
253  // Check if sha256 is not empty
254  if (array_key_exists('sha256', $hashes) && !empty($hashes['sha256'])) {
255  $serializedhash[] = $this->reportGenerator->createHash('SHA-256', $hashes['sha256']);
256  }
257 
258  $allCopyrights = array();
259  foreach ($filesWithLicenses as $fileNode) {
260  $fileCopyrights = $fileNode->getCopyrights();
261  if (!empty($fileCopyrights)) {
262  $allCopyrights = array_merge($allCopyrights, $fileCopyrights);
263  }
264  }
265  $allCopyrights = array_unique($allCopyrights);
266 
267  $reportInfo = $this->uploadDao->getReportInfo($uploadId);
268  $componentVersion = ($reportInfo['ri_version'] ?? '');
269  if ($componentVersion == 'NA') {
270  $componentVersion = '';
271  }
272  $componentId = ($reportInfo['ri_component_id'] ?? '');
273  if ($componentId == 'NA') {
274  $componentId = '';
275  }
276  $componentType = intval($reportInfo['ri_component_type'] ?? 0);
277  $generalAssessment = ($reportInfo['ri_general_assesment'] ?? '');
278  if ($generalAssessment == 'NA') {
279  $generalAssessment = '';
280  }
281 
282  $purl = '';
283  $externalReferences = [];
284  if (!empty($componentId)) {
285  if ($componentType === ComponentType::PURL || $componentType === ComponentType::PACKAGEURL) {
286  $purl = $componentId;
287  } else {
288  $externalReferences[] = [
289  'type' => 'distribution',
290  'url' => $componentId
291  ];
292  }
293  }
294 
295  $maincomponentData = array (
296  'bomref' => strval($uploadId),
297  'type' => 'library',
298  'name' => $upload->getFilename(),
299  'version' => $componentVersion,
300  'hashes' => $serializedhash,
301  'scope' => 'required',
302  'mimeType' => $this->getMimeType($uploadId),
303  'copyright' => implode("\n", $allCopyrights),
304  'description' => $generalAssessment,
305  'purl' => $purl,
306  'externalReferences' => $externalReferences,
307  'licenses' => $mainLicenses
308  );
309  $maincomponent = $this->reportGenerator->createComponent($maincomponentData);
310 
311  $bomdata = array (
312  'tool-version' => $SysConf['BUILD']['VERSION'],
313  'maincomponent' => $maincomponent,
314  'components' => $components,
315  'externalReferences' => $externalReferences
316  );
317 
318  return $this->reportGenerator->generateReport($bomdata);
319  }
320 
328  protected function generateFileComponents($filesWithLicenses, $treeTableName, $uploadId, $itemTreeBounds, $customLicenseTexts = array())
329  {
330  /* @var $treeDao TreeDao */
331  $treeDao = $this->container->get('dao.tree');
332 
333  $filesProceeded = 0;
334  $lastValue = 0;
335  $components = array();
336  foreach ($filesWithLicenses as $fileId => $licenses) {
337  $filesProceeded += 1;
338  if (($filesProceeded & 2047) == 0) {
339  $this->heartbeat($filesProceeded - $lastValue);
340  $lastValue = $filesProceeded;
341  }
342 
343  $hashes = $treeDao->getItemHashes($fileId);
344  $serializedhash = array();
345  $serializedhash[] = $this->reportGenerator->createHash('SHA-1', $hashes['sha1']);
346  $serializedhash[] = $this->reportGenerator->createHash('MD5', $hashes['md5']);
347  // Check if sha256 is not empty
348  if (array_key_exists('sha256', $hashes) && !empty($hashes['sha256'])) {
349  $serializedhash[] = $this->reportGenerator->createHash('SHA-256', $hashes['sha256']);
350  }
351 
352  $fileName = $treeDao->getFullPath($fileId, $treeTableName, 0);
353  $licensesfound = [];
354 
355  if (!empty($licenses->getConcludedLicenses())) {
356  foreach ($licenses->getConcludedLicenses() as $licenseId) {
357  if (array_key_exists($licenseId, $this->licensesInDocument)) {
358  $licObj = $this->licensesInDocument[$licenseId]->getLicenseObj();
359  $isCustomText = $this->licensesInDocument[$licenseId]->isCustomText();
360  $licensedata = $this->getLicenseDataForCycloneDX($licObj, $licenseId, $customLicenseTexts, $isCustomText, false);
361  $licensesfound[] = $this->reportGenerator->createLicense($licensedata);
362  }
363  }
364  } else {
365  foreach ($licenses->getScanners() as $licenseId) {
366  if (array_key_exists($licenseId, $this->licensesInDocument)) {
367  $licObj = $this->licensesInDocument[$licenseId]->getLicenseObj();
368  $isCustomText = $this->licensesInDocument[$licenseId]->isCustomText();
369  $licensedata = $this->getLicenseDataForCycloneDX($licObj, $licenseId, $customLicenseTexts, $isCustomText, false);
370  $licensesfound[] = $this->reportGenerator->createLicense($licensedata);
371  }
372  }
373  }
374  if (!empty($fileName)) {
375  $mimeType = $this->getFileMimeType($fileId, $treeTableName);
376  $componentdata = array(
377  'bomref' => $uploadId .'-'. $fileId,
378  'type' => 'file',
379  'name' => $fileName,
380  'hashes' => $serializedhash,
381  'mimeType' => $mimeType,
382  'copyright' => implode("\n", $licenses->getCopyrights()),
383  'licenses' => $licensesfound,
384  'acknowledgements' => implode("\n", $licenses->getAcknowledgements()),
385  'comments' => implode("\n", $licenses->getComments())
386  );
387  $components[] = $this->reportGenerator->createComponent($componentdata);
388  }
389  }
390  $this->heartbeat($filesProceeded - $lastValue);
391  return $components;
392  }
393 
399  protected function writeReport($packageNodes, $uploadId)
400  {
401  $fileBase = dirname($this->uri);
402 
403  if (!is_dir($fileBase)) {
404  mkdir($fileBase, 0777, true);
405  }
406  umask(0133);
407 
408  $contents = json_encode($packageNodes, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
409  // To ensure the file is valid, replace any non-printable characters with a question mark.
410  // 'Non-printable' is ASCII < 0x20 (excluding \r, \n and tab) and 0x7F - 0x9F.
411  $contents = preg_replace('/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F-\x9F]/u','?',$contents);
412  file_put_contents($this->uri, $contents);
413  $this->updateReportTable($uploadId, $this->jobId, $this->uri);
414  }
415 
422  protected function updateReportTable($uploadId, $jobId, $fileName)
423  {
424  $this->reportutils->updateOrInsertReportgenEntry($uploadId, $jobId, $fileName);
425  }
426 
436  private function getLicenseDataForCycloneDX($licObj, $licenseId, $customLicenseTexts, $isCustomText = false, $includeText = true)
437  {
438  $customText = array_key_exists($licenseId, $customLicenseTexts) ? $customLicenseTexts[$licenseId] : null;
439  $licText = !empty($customText) ? $customText : $licObj->getText();
440 
441  $licensedata = array(
442  'url' => $licObj->getUrl()
443  );
444 
445  if (!empty($customText) || $isCustomText) {
446  if (!empty($customText)) {
447  $prefix = \Fossology\Lib\Data\LicenseRef::SPDXREF_PREFIX;
448  $licensedata['name'] = $prefix . $licObj->getShortName() . '-' . md5($customText);
449  } else {
450  $licensedata['name'] = $licObj->getShortName();
451  }
452  } else {
453  $spdxId = $licObj->getSpdxId();
454  if (!empty($spdxId)) {
455  $licensedata['id'] = $spdxId;
456  } else {
457  $licensedata['name'] = $licObj->getFullName();
458  }
459  }
460 
461  if ($includeText && !empty($licText)) {
462  $licensedata['textContent'] = base64_encode($licText);
463  $licensedata['textContentType'] = 'text/plain';
464  }
465 
466  return $licensedata;
467  }
468 
473  protected function getMimeType($uploadId)
474  {
475  $sql = "SELECT mimetype_name
476  FROM upload u
477  JOIN pfile pf ON u.pfile_fk = pf.pfile_pk
478  JOIN mimetype m ON pf.pfile_mimetypefk = m.mimetype_pk
479  WHERE u.upload_pk = $1";
480 
481  $row = $this->dbManager->getSingleRow($sql, [$uploadId], __METHOD__);
482  return $row['mimetype_name'];
483  }
484 
491  protected function getFileMimeType($fileId, $treeTableName)
492  {
493  $sql = "SELECT m.mimetype_name
494  FROM $treeTableName ut
495  JOIN pfile pf ON ut.pfile_fk = pf.pfile_pk
496  LEFT JOIN mimetype m ON pf.pfile_mimetypefk = m.mimetype_pk
497  WHERE ut.uploadtree_pk = $1";
498 
499  $row = $this->dbManager->getSingleRow($sql, [$fileId], __METHOD__);
500  return $row['mimetype_name'] ?? 'application/octet-stream';
501  }
502 }
503 
504 $agent = new CycloneDXAgent();
505 $agent->scheduler_connect();
506 $agent->run_scheduler_event_loop();
507 $agent->scheduler_disconnect(0);
const OUTPUT_FORMAT_KEY
Argument key for output format.
Definition: cyclonedx.php:44
getFileMimeType($fileId, $treeTableName)
Get the mime type of a file.
Definition: cyclonedx.php:491
writeReport($packageNodes, $uploadId)
Write the report the file and update report table.
Definition: cyclonedx.php:399
updateReportTable($uploadId, $jobId, $fileName)
Update the reportgen table with new report path.
Definition: cyclonedx.php:422
renderPackage($uploadId)
Given an upload id, render the report string.
Definition: cyclonedx.php:189
getMimeType($uploadId)
Get the mime type of the upload.
Definition: cyclonedx.php:473
processUploadId($uploadId)
Given an upload ID, process the items in it.
Definition: cyclonedx.php:141
getUri($fileBase)
Get the URI for the given package.
Definition: cyclonedx.php:158
const DEFAULT_OUTPUT_FORMAT
Default output format.
Definition: cyclonedx.php:45
computeUri($uploadId)
For a given upload, compute the URI.
Definition: cyclonedx.php:173
getLicenseDataForCycloneDX($licObj, $licenseId, $customLicenseTexts, $isCustomText=false, $includeText=true)
Helper to create license data array taking custom text into account.
Definition: cyclonedx.php:436
generateFileComponents($filesWithLicenses, $treeTableName, $uploadId, $itemTreeBounds, $customLicenseTexts=array())
Generate the components by files.
Definition: cyclonedx.php:328
Structure of an Agent with all required parameters.
Definition: Agent.php:41
heartbeat($newProcessed)
Send hear beat to the scheduler.
Definition: Agent.php:203
Wrapper class for license map.
Definition: LicenseMap.php:19
char * trim(char *ptext)
Trimming whitespace.
Definition: fossconfig.c:690
int jobId
The id of the job.
fo_dbManager * dbManager
fo_dbManager object
Definition: process.c:16
FUNCTION char * strtoupper(char *s)
Helper function to upper case a string.
Definition: utils.c:103
Namespace used by CycloneDX agent.