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 
60  protected $citations = [];
65  private $reportutils;
69  private $uploadDao;
73  private $clearingDao;
78  private $licenseDao;
82  protected $dbManager;
86  private $licenseMap;
90  protected $agentNames = AgentRef::AGENT_LIST;
94  protected $uri;
99  private $licensesInDocument = [];
107  private $packageName;
108 
109  function __construct()
110  {
111  // deduce the agent name from the command line arguments
112  $args = getopt("", array(
113  self::OUTPUT_FORMAT_KEY.'::',
114  self::UPLOADS_ADD_KEY.'::'
115  ));
116  $agentName = "";
117  if (array_key_exists(self::OUTPUT_FORMAT_KEY, $args)) {
118  $agentName = trim($args[self::OUTPUT_FORMAT_KEY]);
119  }
120  if (empty($agentName)) {
122  }
123  if (array_key_exists(self::UPLOADS_ADD_KEY, $args)) {
124  $uploadsString = $args[self::UPLOADS_ADD_KEY];
125  if (!empty($uploadsString)) {
126  $this->additionalUploads = explode(',', $uploadsString);
127  }
128  }
129 
130  parent::__construct($agentName, AGENT_VERSION, AGENT_REV);
131 
132  $this->uploadDao = $this->container->get('dao.upload');
133  $this->clearingDao = $this->container->get('dao.clearing');
134  $this->licenseDao = $this->container->get('dao.license');
135  $this->dbManager = $this->container->get('db.manager');
136 
137  $this->reportutils = new ReportUtils();
138  $this->reportGenerator = new BomReportGenerator();
139  }
140 
145  function processUploadId($uploadId)
146  {
147  $this->licenseMap = new LicenseMap($this->dbManager, $this->groupId, LicenseMap::REPORT, true);
148 
149  $packageNodes = $this->renderPackage($uploadId);
150 
151  $this->computeUri($uploadId);
152 
153  $this->writeReport($packageNodes, $uploadId);
154  return true;
155  }
156 
162  protected function getUri($fileBase)
163  {
164  if (count($this->additionalUploads) > 0) {
165  $fileName = $fileBase . "multifile" . "_" . strtoupper($this->outputFormat);
166  } else {
167  $fileName = $fileBase. strtoupper($this->outputFormat)."_".$this->packageName;
168  }
169 
170  return $fileName .".json" ;
171  }
172 
177  protected function computeUri($uploadId)
178  {
179  global $SysConf;
180  $upload = $this->uploadDao->getUpload($uploadId);
181  $this->packageName = $upload->getFilename();
182 
183  $fileBase = $SysConf['FOSSOLOGY']['path']."/report/";
184 
185  $this->uri = $this->getUri($fileBase);
186  }
187 
193  protected function renderPackage($uploadId)
194  {
195  global $SysConf;
196  $uploadTreeTableName = $this->uploadDao->getUploadtreeTableName($uploadId);
197  $itemTreeBounds = $this->uploadDao->getParentItemBounds($uploadId, $uploadTreeTableName);
198  $this->heartbeat(0);
199 
200  $filesWithLicenses = $this->reportutils
201  ->getFilesWithLicensesFromClearings($itemTreeBounds, $this->groupId,
202  $this, $this->licensesInDocument);
203  $this->heartbeat(0);
204 
205  $this->reportutils->addClearingStatus($filesWithLicenses, $itemTreeBounds, $this->groupId);
206  $this->heartbeat(0);
207 
208  $this->reportutils->addScannerResults($filesWithLicenses, $itemTreeBounds, $this->groupId, $this->licensesInDocument);
209  $this->heartbeat(0);
210 
211  $this->reportutils->addCopyrightResults($filesWithLicenses, $uploadId);
212  $this->heartbeat(0);
213 
214  $this->citations = [
215  'cite-scanner' => [
216  'timestamp' => date('c'),
217  'attributedTo' => 'tool-fossology-scanners',
218  'expressions' => []
219  ],
220  'cite-analyst' => [
221  'timestamp' => date('c'),
222  'attributedTo' => 'person-fossology-analyst',
223  'expressions' => []
224  ]
225  ];
226 
227  $customLicenseTexts = $this->clearingDao->getMainLicenseReportInfos($uploadId, $this->groupId);
228 
229  $upload = $this->uploadDao->getUpload($uploadId);
230  $components = $this->generateFileComponents($filesWithLicenses, $upload->getTreeTableName(), $uploadId, $itemTreeBounds, $customLicenseTexts);
231 
232  $mainLicenseIds = $this->clearingDao->getMainLicenseIds($uploadId, $this->groupId);
233  $mainLicenses = array();
234  $seenLicenseIds = array();
235  foreach ($mainLicenseIds as $licId) {
236  $reportedLicenseId = $this->licenseMap->getProjectedId($licId);
237  $mainLicObj = $this->licenseDao->getLicenseById($reportedLicenseId, $this->groupId);
238  if ($mainLicObj === null) {
239  continue;
240  }
241 
242  $licensedata = $this->getLicenseDataForCycloneDX($mainLicObj, $licId, $customLicenseTexts);
243  $licensedata['bom-ref'] = 'lic-clearing-main-' . $licId;
244  $licensedata['acknowledgement'] = 'concluded';
245  $this->citations['cite-analyst']['expressions'][] = '$..[?(@.bom-ref=="lic-clearing-main-' . $licId . '")]';
246  $mainLicenses[] = $this->reportGenerator->createLicense($licensedata);
247 
248  $customText = array_key_exists($licId, $customLicenseTexts) ? $customLicenseTexts[$licId] : null;
249  $licText = !empty($customText) ? $customText : $mainLicObj->getText();
250  $reportLicId = $mainLicObj->getId() . "-" . md5($licText);
251  $seenLicenseIds[$reportLicId] = true;
252  }
253 
254  foreach ($filesWithLicenses as $fileNode) {
255  $isConcluded = !empty($fileNode->getConcludedLicenses());
256  $licenseIds = $isConcluded
257  ? $fileNode->getConcludedLicenses()
258  : $fileNode->getScanners();
259  foreach ($licenseIds as $licenseId) {
260  if (array_key_exists($licenseId, $this->licensesInDocument) && !array_key_exists($licenseId, $seenLicenseIds)) {
261  $seenLicenseIds[$licenseId] = true;
262  $licObj = $this->licensesInDocument[$licenseId]->getLicenseObj();
263  $isCustomText = $this->licensesInDocument[$licenseId]->isCustomText();
264  $licensedata = $this->getLicenseDataForCycloneDX($licObj, $licenseId, $customLicenseTexts, $isCustomText);
265  $refPrefix = $isConcluded ? 'clearing' : 'scanner';
266  $licensedata['bom-ref'] = 'lic-' . $refPrefix . '-main-' . $licenseId;
267  $licensedata['acknowledgement'] = $isConcluded ? 'concluded' : 'declared';
268  $this->citations['cite-' . ($isConcluded ? 'analyst' : 'scanner')]['expressions'][] = '$..[?(@.bom-ref=="lic-' . $refPrefix . '-main-' . $licenseId . '")]';
269  $mainLicenses[] = $this->reportGenerator->createLicense($licensedata);
270  }
271  }
272  }
273 
274  $hashes = $this->uploadDao->getUploadHashes($uploadId);
275  $serializedhash = array();
276  $serializedhash[] = $this->reportGenerator->createHash('SHA-1', $hashes['sha1']);
277  $serializedhash[] = $this->reportGenerator->createHash('MD5', $hashes['md5']);
278  // Check if sha256 is not empty
279  if (array_key_exists('sha256', $hashes) && !empty($hashes['sha256'])) {
280  $serializedhash[] = $this->reportGenerator->createHash('SHA-256', $hashes['sha256']);
281  }
282 
283  $allCopyrights = array();
284  foreach ($filesWithLicenses as $fileNode) {
285  $fileCopyrights = $fileNode->getCopyrights();
286  if (!empty($fileCopyrights)) {
287  $allCopyrights = array_merge($allCopyrights, $fileCopyrights);
288  }
289  }
290  $allCopyrights = array_unique($allCopyrights);
291 
292  $reportInfo = $this->uploadDao->getReportInfo($uploadId);
293  $componentVersion = ($reportInfo['ri_version'] ?? '');
294  if ($componentVersion == 'NA') {
295  $componentVersion = '';
296  }
297  $componentId = ($reportInfo['ri_component_id'] ?? '');
298  if ($componentId == 'NA') {
299  $componentId = '';
300  }
301  $componentType = intval($reportInfo['ri_component_type'] ?? 0);
302  $generalAssessment = ($reportInfo['ri_general_assesment'] ?? '');
303  if ($generalAssessment == 'NA') {
304  $generalAssessment = '';
305  }
306 
307  $purl = '';
308  $externalReferences = [];
309  if (!empty($componentId)) {
310  if ($componentType === ComponentType::PURL || $componentType === ComponentType::PACKAGEURL) {
311  $purl = $componentId;
312  } else {
313  $externalReferences[] = [
314  'type' => 'distribution',
315  'url' => $componentId
316  ];
317  }
318  }
319 
320  $maincomponentData = array (
321  'bomref' => strval($uploadId),
322  'type' => 'library',
323  'name' => $upload->getFilename(),
324  'version' => $componentVersion,
325  'hashes' => $serializedhash,
326  'scope' => 'required',
327  'mimeType' => $this->getMimeType($uploadId),
328  'copyright' => implode("\n", $allCopyrights),
329  'description' => $generalAssessment,
330  'purl' => $purl,
331  'externalReferences' => $externalReferences,
332  'licenses' => $mainLicenses
333  );
334  $maincomponent = $this->reportGenerator->createComponent($maincomponentData);
335 
336  $formattedDate = date('Y-m-d\TH:i:s\Z');
337 
338  $finalCitations = [];
339  if (!empty($this->citations['cite-scanner']['expressions'])) {
340  $finalCitations[] = $this->citations['cite-scanner'];
341  }
342  if (!empty($this->citations['cite-analyst']['expressions'])) {
343  $finalCitations[] = $this->citations['cite-analyst'];
344  }
345 
346  $bomdata = array(
347  'timestamp' => $formattedDate,
348  'tool-version' => $SysConf['BUILD']['VERSION'],
349  'maincomponent' => $maincomponent,
350  'components' => $components,
351  'externalReferences' => $externalReferences,
352  'citations' => $finalCitations
353  );
354 
355  return $this->reportGenerator->generateReport($bomdata);
356  }
357 
365  protected function generateFileComponents($filesWithLicenses, $treeTableName, $uploadId, $itemTreeBounds, $customLicenseTexts = array())
366  {
367  /* @var $treeDao TreeDao */
368  $treeDao = $this->container->get('dao.tree');
369 
370  $stateWoInfos = $this->getCycloneDXReportConf($uploadId, 1);
371 
372  $filesProceeded = 0;
373  $lastValue = 0;
374  $components = array();
375  foreach ($filesWithLicenses as $fileId => $licenses) {
376  $filesProceeded += 1;
377  if (($filesProceeded & 2047) == 0) {
378  $this->heartbeat($filesProceeded - $lastValue);
379  $lastValue = $filesProceeded;
380  }
381 
382  if ($stateWoInfos && empty($licenses->getConcludedLicenses()) &&
383  empty($licenses->getScanners()) && empty($licenses->getCopyrights())) {
384  continue;
385  }
386 
387  $hashes = $treeDao->getItemHashes($fileId);
388  $serializedhash = array();
389  $serializedhash[] = $this->reportGenerator->createHash('SHA-1', $hashes['sha1']);
390  $serializedhash[] = $this->reportGenerator->createHash('MD5', $hashes['md5']);
391  // Check if sha256 is not empty
392  if (array_key_exists('sha256', $hashes) && !empty($hashes['sha256'])) {
393  $serializedhash[] = $this->reportGenerator->createHash('SHA-256', $hashes['sha256']);
394  }
395 
396  $fileName = $treeDao->getFullPath($fileId, $treeTableName, 0);
397  $licensesfound = [];
398 
399  if (!empty($licenses->getConcludedLicenses())) {
400  foreach ($licenses->getConcludedLicenses() as $licenseId) {
401  if (array_key_exists($licenseId, $this->licensesInDocument)) {
402  $licObj = $this->licensesInDocument[$licenseId]->getLicenseObj();
403  $isCustomText = $this->licensesInDocument[$licenseId]->isCustomText();
404  $licensedata = $this->getLicenseDataForCycloneDX($licObj, $licenseId, $customLicenseTexts, $isCustomText, $stateOsselot);
405  $licensedata['bom-ref'] = 'lic-clearing-' . $licenseId . '-' . $fileId;
406  $licensedata['acknowledgement'] = 'concluded';
407  $licensesfound[] = $this->reportGenerator->createLicense($licensedata, false);
408  }
409  }
410  } else {
411  foreach ($licenses->getScanners() as $licenseId) {
412  if (array_key_exists($licenseId, $this->licensesInDocument)) {
413  $licObj = $this->licensesInDocument[$licenseId]->getLicenseObj();
414  $isCustomText = $this->licensesInDocument[$licenseId]->isCustomText();
415  $licensedata = $this->getLicenseDataForCycloneDX($licObj, $licenseId, $customLicenseTexts, $isCustomText, $stateOsselot);
416  $licensedata['bom-ref'] = 'lic-scanner-' . $licenseId . '-' . $fileId;
417  $licensedata['acknowledgement'] = 'declared';
418  $licensesfound[] = $this->reportGenerator->createLicense($licensedata, false);
419  }
420  }
421  }
422  if (!empty($fileName)) {
423  $mimeType = $this->getFileMimeType($fileId, $treeTableName);
424  $componentdata = array(
425  'bomref' => $uploadId .'-'. $fileId,
426  'type' => 'file',
427  'name' => $fileName,
428  'hashes' => $serializedhash,
429  'mimeType' => $mimeType,
430  'copyright' => implode("\n", $licenses->getCopyrights()),
431  'licenses' => $licensesfound,
432  'acknowledgements' => implode("\n", $licenses->getAcknowledgements()),
433  'comments' => implode("\n", $licenses->getComments())
434  );
435  $components[] = $this->reportGenerator->createComponent($componentdata);
436  }
437  }
438  $this->heartbeat($filesProceeded - $lastValue);
439  return $components;
440  }
441 
447  protected function writeReport($packageNodes, $uploadId)
448  {
449  $fileBase = dirname($this->uri);
450 
451  if (!is_dir($fileBase)) {
452  mkdir($fileBase, 0777, true);
453  }
454  umask(0133);
455 
456  $contents = json_encode($packageNodes, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
457  // To ensure the file is valid, replace any non-printable characters with a question mark.
458  // 'Non-printable' is ASCII < 0x20 (excluding \r, \n and tab) and 0x7F - 0x9F.
459  $contents = preg_replace('/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F-\x9F]/u','?',$contents);
460  file_put_contents($this->uri, $contents);
461  $this->updateReportTable($uploadId, $this->jobId, $this->uri);
462  }
463 
470  protected function updateReportTable($uploadId, $jobId, $fileName)
471  {
472  $this->reportutils->updateOrInsertReportgenEntry($uploadId, $jobId, $fileName);
473  }
474 
484  private function getLicenseDataForCycloneDX($licObj, $licenseId, $customLicenseTexts, $isCustomText = false, $includeText = true)
485  {
486  $customText = array_key_exists($licenseId, $customLicenseTexts) ? $customLicenseTexts[$licenseId] : null;
487  $licText = !empty($customText) ? $customText : $licObj->getText();
488 
489  $licensedata = array(
490  'url' => $licObj->getUrl()
491  );
492 
493  if (!empty($customText) || $isCustomText) {
494  if (!empty($customText)) {
495  $prefix = \Fossology\Lib\Data\LicenseRef::SPDXREF_PREFIX;
496  $licensedata['name'] = $prefix . $licObj->getShortName() . '-' . md5($customText);
497  } else {
498  $licensedata['name'] = $licObj->getShortName();
499  }
500  } else {
501  $spdxId = $licObj->getSpdxId();
502  if (!empty($spdxId)) {
503  $licensedata['id'] = $spdxId;
504  } else {
505  $licensedata['name'] = $licObj->getFullName();
506  }
507  }
508 
509  if ($includeText && !empty($licText)) {
510  $licensedata['textContent'] = base64_encode($licText);
511  $licensedata['textContentType'] = 'text/plain';
512  }
513 
514  return $licensedata;
515  }
516 
521  protected function getMimeType($uploadId)
522  {
523  $sql = "SELECT mimetype_name
524  FROM upload u
525  JOIN pfile pf ON u.pfile_fk = pf.pfile_pk
526  JOIN mimetype m ON pf.pfile_mimetypefk = m.mimetype_pk
527  WHERE u.upload_pk = $1";
528 
529  $row = $this->dbManager->getSingleRow($sql, [$uploadId], __METHOD__);
530  return $row['mimetype_name'];
531  }
532 
539  protected function getFileMimeType($fileId, $treeTableName)
540  {
541  $sql = "SELECT m.mimetype_name
542  FROM $treeTableName ut
543  JOIN pfile pf ON ut.pfile_fk = pf.pfile_pk
544  LEFT JOIN mimetype m ON pf.pfile_mimetypefk = m.mimetype_pk
545  WHERE ut.uploadtree_pk = $1";
546 
547  $row = $this->dbManager->getSingleRow($sql, [$fileId], __METHOD__);
548  return $row['mimetype_name'] ?? 'application/octet-stream';
549  }
550 
559  protected function getCycloneDXReportConf($uploadId, $key)
560  {
561  $settings = $this->uploadDao->getCyclonedxSettings($uploadId);
562  if (!empty($settings)) {
563  $settingsArr = explode(',', $settings);
564  if (isset($settingsArr[$key]) && $settingsArr[$key] === "checked") {
565  return true;
566  }
567  }
568  return false;
569  }
570 }
571 
572 $agent = new CycloneDXAgent();
573 $agent->scheduler_connect();
574 $agent->run_scheduler_event_loop();
575 $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:539
writeReport($packageNodes, $uploadId)
Write the report the file and update report table.
Definition: cyclonedx.php:447
updateReportTable($uploadId, $jobId, $fileName)
Update the reportgen table with new report path.
Definition: cyclonedx.php:470
renderPackage($uploadId)
Given an upload id, render the report string.
Definition: cyclonedx.php:193
getCycloneDXReportConf($uploadId, $key)
Get CycloneDX report conf state for a given upload.
Definition: cyclonedx.php:559
getMimeType($uploadId)
Get the mime type of the upload.
Definition: cyclonedx.php:521
processUploadId($uploadId)
Given an upload ID, process the items in it.
Definition: cyclonedx.php:145
getUri($fileBase)
Get the URI for the given package.
Definition: cyclonedx.php:162
const DEFAULT_OUTPUT_FORMAT
Default output format.
Definition: cyclonedx.php:45
computeUri($uploadId)
For a given upload, compute the URI.
Definition: cyclonedx.php:177
getLicenseDataForCycloneDX($licObj, $licenseId, $customLicenseTexts, $isCustomText=false, $includeText=true)
Helper to create license data array taking custom text into account.
Definition: cyclonedx.php:484
generateFileComponents($filesWithLicenses, $treeTableName, $uploadId, $itemTreeBounds, $customLicenseTexts=array())
Generate the components by files.
Definition: cyclonedx.php:365
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.