sagacity/ste/export.php
Ryan Prather 99eb5342cf
fix(eChecklist-import): Fix error with formulas in status column
This should remedy the reported behavior of statuses quietly being changed to "Not Reviewed".
scan.inc - Added new set_Host_Error method to set the error value for a specific host
parse_excel_echecklist.php - explicitly check for the status to equal 1-of-7 expected values, if not, add note to finding, set scan error message, and default status to "Not Reviewed"
export.php - Added cell lock for A11:E{last row} with the default password of "sagacity" (all lowercase)

Fixes #80
2019-01-09 21:49:58 -05:00

535 lines
19 KiB
PHP

<?php
/**
* File: export.php
* Author: Ryan Prather
* Purpose: Export findings to an Excel spreadsheet eChecklist
* Created: Oct 15, 2013
*
* Portions Copyright 2016-2018: Cyber Perspectives, LLC, All rights reserved
* Released under the Apache v2.0 License
*
* Portions Copyright (c) 2012-2015, Salient Federal Solutions
* Portions Copyright (c) 2008-2011, Science Applications International Corporation (SAIC)
* Released under Modified BSD License
*
* See license.txt for details
*
* Change Log:
* - Oct 15, 2013 - File created
* - Dec 12, 2016 - Added Cyber Perspectives license, changed writing to spreadsheet to use constants for company data, and
* Added ST&E ending date to cover page
* - Mar 9, 2017 - Fixed issue with export overwriting columns
* - Apr 15, 2017 - Set text wrapping if enabled on Short Title column
* - May 13, 2017 - Migrated to PHPSpreadsheet library, and add support for other export formats
* - Jun 3, 2017 - Fixed bug #232
* - Jul 23, 2017 - MAS Added comments and rudimentary RMF control support to eChecklist export
* - Dec 27, 2017 - Updating classification info on cover sheet page,
* removed classification from G2,
* fixed invalid function call to stringFromColumnIndex as it was moved to a different class and changed to 1-based instead of 0-based,
* syntax updates, updated PDF writer to Tcpdf class, added die if constant ECHECKLIST_FORMAT is not set as expected
* - Jan 15, 2018 - Formatting, updated use statements, not seeing behavior explained in #373
* - Nov 8, 2018 - Minor change to OS listing and added add_cell_comment method to migrate scanner notes to a comment instead of the main note (separating the scanner and anaylst comments)
*/
include_once 'config.inc';
include_once 'database.inc';
include_once 'helper.inc';
require_once 'vendor/autoload.php';
require_once 'excelConditionalStyles.inc';
use PhpOffice\PhpSpreadsheet\Writer\Xlsx;
use PhpOffice\PhpSpreadsheet\Writer\Xls;
use PhpOffice\PhpSpreadsheet\Writer\Ods;
use PhpOffice\PhpSpreadsheet\Writer\Csv;
use PhpOffice\PhpSpreadsheet\Writer\Html;
use PhpOffice\PhpSpreadsheet\Cell\Coordinate;
use PhpOffice\PhpSpreadsheet\Worksheet;
use Monolog\Logger;
use Monolog\Handler\StreamHandler;
global $conditions, $validation, $borders;
set_time_limit(0);
$db = new db();
$emass_ccis = null;
$log_level = convert_log_level();
$chk_hosts = filter_input_array(INPUT_POST, 'chk_host');
$cat_id = filter_input(INPUT_GET, 'cat', FILTER_VALIDATE_INT, FILTER_NULL_ON_FAILURE);
if (!$cat_id) {
$cat_id = filter_input(INPUT_POST, 'cat', FILTER_VALIDATE_INT, FILTER_NULL_ON_FAILURE);
}
$ste_id = filter_input(INPUT_COOKIE, 'ste', FILTER_VALIDATE_INT, FILTER_NULL_ON_FAILURE);
if (!$ste_id) {
$ste_id = filter_input(INPUT_POST, 'ste', FILTER_VALIDATE_INT, FILTER_NULL_ON_FAILURE);
}
if (!$ste_id || !$cat_id) {
die("Could not find the STE or Category ID");
}
$cat = $db->get_Category($cat_id)[0];
if (!is_a($cat, 'ste_cat')) {
die("Error finding category $cat_id");
}
$ste = $db->get_STE($ste_id)[0];
if (!is_a($ste, 'ste')) {
die("Error finding ST&E");
}
$log = new Logger("eChecklist-export");
$log->pushHandler(new StreamHandler(LOG_PATH . "/{$cat->get_Name()}-echecklist-export.log", $log_level));
if ($chk_hosts) {
$findings = $db->get_Category_Findings($cat_id, $chk_hosts);
}
else {
$findings = $db->get_Category_Findings($cat_id);
}
$log->debug("Got findings");
// Get mapping of eMASS controls to CCIs from DB
if ($ste->get_System()->get_Accreditation_Type() == accrediation_types::RMF) {
$emass_ccis = $db->get_EMASS_CCIs();
}
$Reader = \PhpOffice\PhpSpreadsheet\IOFactory::createReaderForFile("eChecklist-Template.xlsx");
$ss = $Reader->load("eChecklist-Template.xlsx");
$log->debug("Loaded template");
$ss->setActiveSheetIndexByName('Cover Sheet')
->setCellValue("B5", "{$ste->get_System()->get_Name()} eChecklist")
->setCellValue("B9", "{$ste->get_Eval_Start_Date()->format("m/d/Y")}-{$ste->get_Eval_End_Date()->format("m/d/Y")}")
->setCellValue("B2", ($ste->get_System()->get_Classification() == 'Classified' ? "SECRET" : "UNCLASSIFIED"))
->setCellValue("B12", "by:\r" . COMPANY . "\r" . COMP_ADD)
->setCellValue("B15", "Derived from: " . SCG . "\rReasons: <reasons>\rDeclassify on: " . DECLASSIFY_ON);
// set properties
$ss->getProperties()
->setCreator(CREATOR);
$ss->getProperties()
->setLastModifiedBy(LAST_MODIFIED_BY);
$ss->getProperties()
->setCompany(COMPANY);
$ss->getProperties()
->setTitle("{$cat->get_Name()} eChecklist");
$ss->getProperties()
->setSubject("{$cat->get_Name()} eChecklist");
$ss->getProperties()
->setDescription("{$cat->get_Name()} eChecklist");
$log->debug("File properties set");
// set active sheet
$ss->setActiveSheetIndex(2);
$host_status = array(
$conditions['open'],
$conditions['exception'],
$conditions['false_positive'],
$conditions['not_a_finding'],
$conditions['not_applicable'],
$conditions['no_data'],
$conditions['not_reviewed'],
$conditions['true'],
$conditions['false']
);
// Iterate over worksheets in the category; populating each with the checklists and finding data
foreach ($findings as $worksheet_name => $data) {
$log->debug("Looping through worksheet $worksheet_name");
$chk_arr = [];
// Build the "Checklist" cell string with titles of all checklists on this worksheet
foreach ($data['checklists'] as $key => $chk_id) {
$checklist = $db->get_Checklist($chk_id)[0];
$chk_arr[] = "{$checklist->get_Name()} V{$checklist->get_Version()}R{$checklist->get_Release()} ({$checklist->get_type()})";
}
$checklist_str = implode(", ", $chk_arr);
if (is_null($sheet = $ss->getSheetByName($worksheet_name))) {
$new_sheet = clone $ss->getSheet(2);
$new_sheet->setTitle($worksheet_name);
$ss->addSheet($new_sheet);
$sheet = $ss->getSheetByName($worksheet_name);
if (is_array($data['target_list']) && count($data['target_list']) > 1) {
$sheet->insertNewColumnBefore("G", count($data['target_list']) - 1);
}
$sheet->setCellValue("B9", $checklist_str);
}
else {
$sheet->setCellValue("B9", "{$sheet->getCellValue("B9")}, {$checklist_str}");
}
$class = 'UNCLASSIFIED';
if (isset($data['highest_class'])) {
switch ($data['highest_class']) {
case 'FOUO':
$class = 'UNCLASSIFIED//FOUO';
break;
case 'S':
$class = 'SECRET';
break;
}
}
else {
if ($ste->get_System()->get_Classification() == 'Sensitive') {
$class = 'UNCLASSIFIED//FOUO';
}
elseif ($ste->get_System()->get_Classification() == 'Classified') {
$class = 'SECRET';
}
}
$log->debug("Setting classification: $class");
$sheet->setCellValue("A1", $class)
->setCellValue('E2', $ste->get_System()->get_Name());
$sheet->getStyle("A1")
->setConditionalStyles([$conditions['unclass_classification'], $conditions['secret_classification']]);
$row = 11;
$last_tgt_col = Coordinate::stringFromColumnIndex(count($data['target_list']) + 5);
$overall_col = Coordinate::stringFromColumnIndex(count($data['target_list']) + 6);
$same_col = Coordinate::stringFromColumnIndex(count($data['target_list']) + 7);
$notes_col = Coordinate::stringFromColumnIndex(count($data['target_list']) + 8);
$check_contents_col = Coordinate::stringFromColumnIndex(count($data['target_list']) + 9);
// Iterate over checklist items ($stig_id) and populate spreadsheet with status of each
foreach ($data['stigs'] as $stig_id => $tgt_status) {
$log->debug("Running through STIG $stig_id", $tgt_status);
$ia_controls_string = null;
// If $do_rmf is set, replace CCIs w/ eMASS RMF Control and build string to
// insert into IA Controls cell, otherwise just use CCIs.
if ($ste->get_System()->get_Accreditation_Type() == accrediation_types::RMF) {
$ia_controls = $tgt_status['echecklist']->get_IA_Controls();
$rmf_controls = [];
foreach ($ia_controls as $control) {
// Remove 'CCI-' and leading zeros
$id = ltrim(substr($control, strpos($control, "-") + 1), '0');
// lookup cci in $emass_ccis
$key = array_search($id, array_column($emass_ccis, 'id'));
// Push the control onto $rmf_controls
array_push($rmf_controls, $emass_ccis[$key]['control']);
}
$ia_controls_string = implode(" ", $rmf_controls);
}
else {
$ia_controls_string = $tgt_status['echecklist']->get_IA_Controls_String();
}
$sheet->setCellValue("A{$row}", $stig_id)
->setCellValue("B{$row}", $tgt_status['echecklist']->get_VMS_ID())
->setCellValue("C{$row}", $tgt_status['echecklist']->get_Cat_Level_String())
->setCellValue("D{$row}", $ia_controls_string)
->setCellValue("E{$row}", deduplicateString($tgt_status['echecklist']->get_Short_Title()));
$log->debug("Added STIG info ($stig_id), not to targets");
foreach ($data['target_list'] as $host_name => $col_id) {
$status = 'Not Applicable';
if (isset($tgt_status["{$host_name}"])) {
$status = $tgt_status["{$host_name}"];
}
$col = Coordinate::stringFromColumnIndex($col_id);
$sheet->setCellValue("{$col}{$row}", $status);
}
$overall_str = "=IF(" .
"COUNTIF(F{$row}:{$last_tgt_col}{$row},\"Open\")+" .
"COUNTIF(F{$row}:{$last_tgt_col}{$row},\"Exception\")" .
">0,\"Open\",\"Not a Finding\")";
$same_str = "=IF(" .
"COUNTIF(F{$row}:{$last_tgt_col}{$row},F{$row})=" .
"COLUMNS(F{$row}:{$last_tgt_col}{$row}), TRUE, FALSE)";
$sheet->setCellValue($overall_col . $row, $overall_str);
$sheet->setCellValue($same_col . $row, $same_str, true)
->getStyle("{$same_col}11:{$same_col}{$sheet->getHighestDataRow()}")
->setConditionalStyles([$conditions['true'], $conditions['false']]);
//->setDataValidation($validation['true_false']);
$sheet->setCellValue($notes_col . $row, deduplicateString($tgt_status['echecklist']->get_Notes()))
->setCellValue($check_contents_col . $row, deduplicateString($tgt_status['echecklist']->get_Check_Contents()));
$log->debug("Added remaining cells");
$row++;
}
$sheet->setDataValidation("F11:{$last_tgt_col}{$row}", clone $validation['host_status']);
$log->debug("Set data validation for target $host_name");
$log->debug("Completed STIG parsing");
$sheet->getStyle("F11:" . Coordinate::stringFromColumnIndex(count($data['target_list']) + 6) . $row)
->setConditionalStyles($host_status);
$sheet->getStyle("C11:C{$sheet->getHighestDataRow()}")
->setConditionalStyles(array($conditions['cat_1'], $conditions['cat_2'], $conditions['cat_3']));
$sheet->getStyle("{$notes_col}11:{$notes_col}{$row}")
->setConditionalStyles(array(
$conditions['open_conflict'],
$conditions['nf_na_conflict']
));
if (is_array($data['target_list']) && count($data['target_list']) > 1) {
$sheet->getStyle("G3:{$notes_col}7")
->getFill()
->setFillType(\PhpOffice\PhpSpreadsheet\Style\Fill::FILL_NONE);
$sheet->getStyle("G2")
->getFill()
->setFillType(\PhpOffice\PhpSpreadsheet\Style\Fill::FILL_NONE);
$sheet->getStyle("G2")
->getFont()
->setBold(false);
$sheet->getStyle("G2")
->getAlignment()
->setHorizontal(\PhpOffice\PhpSpreadsheet\Style\Alignment::HORIZONTAL_LEFT);
}
$sheet->getStyle("A1:{$sheet->getHighestDataColumn()}{$sheet->getHighestDataRow()}")
->applyFromArray($borders);
$sheet->freezePane("A11");
$sheet->setAutoFilter("A10:{$sheet->getHighestDataColumn()}10");
$sheet->protectCellsByColumnAndRow(1, 11, 5, $sheet->getHighestDataRow(), "sagacity");
updateHostHeader($sheet, $data['target_list'], $db);
$log->debug("Completed worksheet $worksheet_name");
}
$ss->removeSheetByIndex(2);
$log->debug("Writing to file");
$ct = '';
$writer = null;
switch (ECHECKLIST_FORMAT) {
case 'xlsx':
$writer = new Xlsx($ss);
$writer->setPreCalculateFormulas(false);
$ct = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet";
break;
case 'ods':
$writer = new Ods($ss);
$writer->setPreCalculateFormulas(false);
$ct = "application/vnd.oasis.opendocument.spreadsheet";
break;
case 'pdf':
\PhpOffice\PhpSpreadsheet\Settings::setPdfRendererName(PhpOffice\PhpSpreadsheet\Settings::PDF_RENDERER_TCPDF);
$writer = new Tcpdf($ss);
$writer->writeAllSheets();
$ct = "application/pdf";
break;
case 'html':
$writer = new Html($ss);
$writer->writeAllSheets();
$writer->setPreCalculateFormulas(false);
$ct = "text/html";
break;
case 'xls':
$writer = new Xls($ss);
$writer->setPreCalculateFormulas(false);
$ct = "application/vnd.ms-excel";
break;
case 'csv':
$writer = new Csv($ss);
$ct = "text/csv";
break;
default:
die("Did not recognize eChecklist format " . ECHECKLIST_FORMAT);
}
$cat_name = str_replace(" ", "_", $cat->get_Name());
header("Content-type: $ct");
header("Content-disposition: attachment; filename='{$cat_name}-eChecklist-{$ste_id}." . ECHECKLIST_FORMAT . "'");
$writer->save("php://output");
$log->debug("Writing complete");
/**
* Update the header on the worksheet
*
* @param Worksheet $sheet
* @param array:integer $tgts
* @param db $db
*/
function updateHostHeader($sheet, $tgts, &$db) {
global $ste_id, $log;
$host_names = [];
$ips = [];
$oses = [];
$open_cat_1 = null;
$open_cat_2 = null;
$open_cat_3 = null;
$not_a_finding = null;
$not_applicable = null;
$not_reviewed = null;
foreach ($tgts as $tgt_name => $col_id) {
$log->notice("tgt_name: $tgt_name\tcol_id: $col_id");
$tgt = $db->get_Target_Details($ste_id, $tgt_name)[0];
/** @var software $os */
$os = $db->get_Software($tgt->get_OS_ID())[0];
$oses[] = $os->get_SW_String();
$host_names[] = $tgt->get_Name();
if (is_array($tgt->interfaces) && count($tgt->interfaces)) {
foreach ($tgt->interfaces as $int) {
if (!in_array($int->get_IPv4(), ["127.0.0.1", "", "0.0.0.0", null])) {
$ips[] = $int->get_IPv4();
break;
}
}
}
$col = Coordinate::stringFromColumnIndex($col_id);
$highest_row = $sheet->getHighestDataRow();
$sheet->getColumnDimension($col)
->setWidth(14.14);
$sheet->setCellValue("{$col}8", "=COUNTIFS({$col}11:{$col}{$highest_row}, \"Open\", \$C\$11:\$C\${$highest_row}, \"I\")")
->setCellValue("{$col}9", "=COUNTIF({$col}11:{$col}{$highest_row}, \"Not Reviewed\")")
->setCellValue("{$col}10", $tgt->get_Name());
$sheet->getStyle("{$col}10")
->getFont()
->setBold(true);
$sheet->getStyle("{$col}10")
->getFill()
->setFillType(\PhpOffice\PhpSpreadsheet\Style\Fill::FILL_SOLID)
->setStartColor($GLOBALS['yellow']);
if (!is_null($open_cat_1)) {
$open_cat_1 .= "+";
$open_cat_2 .= "+";
$open_cat_3 .= "+";
$not_a_finding .= "+";
$not_applicable .= "+";
$not_reviewed .= "+";
}
else {
$open_cat_1 = "=";
$open_cat_2 = "=";
$open_cat_3 = "=";
$not_a_finding = "=";
$not_applicable = "=";
$not_reviewed = "=";
}
$open_cat_1 .= "COUNTIFS({$col}11:{$col}{$highest_row}, \"Open\", \$C\$11:\$C\${$highest_row}, \"I\")";
$open_cat_2 .= "COUNTIFS({$col}11:{$col}{$highest_row}, \"Open\", \$C\$11:\$C\${$highest_row}, \"II\")";
$open_cat_3 .= "COUNTIFS({$col}11:{$col}{$highest_row}, \"Open\", \$C\$11:\$C\${$highest_row}, \"III\")";
$not_a_finding .= "COUNTIF({$col}11:{$col}{$highest_row}, \"Not a Finding\")";
$not_applicable .= "COUNTIF({$col}11:{$col}{$highest_row}, \"Not Applicable\")";
$not_reviewed .= "COUNTIF({$col}11:{$col}{$highest_row}, \"Not Reviewed\")";
}
$overall_col = Coordinate::stringFromColumnIndex(count($tgts) + 6);
$same_col = Coordinate::stringFromColumnIndex(count($tgts) + 7);
$notes_col = Coordinate::stringFromColumnIndex(count($tgts) + 8);
$check_contents_col = Coordinate::stringFromColumnIndex(count($tgts) + 9);
$sheet->getStyle("{$overall_col}8:{$same_col}8")
->getFill()
->setFillType(\PhpOffice\PhpSpreadsheet\Style\Fill::FILL_SOLID)
->setStartColor($GLOBALS['orange']);
$sheet->getStyle("{$overall_col}9:{$same_col}9")
->getFill()
->setFillType(\PhpOffice\PhpSpreadsheet\Style\Fill::FILL_SOLID)
->setStartColor($GLOBALS['green']);
$sheet->getStyle("{$overall_col}10:{$same_col}10")
->getFill()
->setFillType(\PhpOffice\PhpSpreadsheet\Style\Fill::FILL_SOLID)
->setStartColor($GLOBALS['yellow']);
$sheet->getStyle("{$notes_col}10:{$check_contents_col}10")
->getFill()
->setFillType(\PhpOffice\PhpSpreadsheet\Style\Fill::FILL_SOLID)
->setStartColor($GLOBALS['light_gray']);
$sheet->setCellValue("{$overall_col}8", "=COUNTIF({$overall_col}11:{$overall_col}{$highest_row}, \"Open\")")
->setCellValue("{$overall_col}9", "=COUNTIF({$overall_col}11:{$overall_col}{$highest_row}, \"Not a Finding\")")
->setCellValue("{$same_col}8", "=COUNTIF({$same_col}11:{$same_col}{$highest_row}, FALSE)")
->setCellValue("{$same_col}9", "=COUNTIF({$same_col}11:{$same_col}{$highest_row}, TRUE)")
->setCellValue("E3", implode(", ", $host_names))
->setCellValue("E4", implode(", ", $ips))
->setCellValue("G4", implode(", ", array_unique($oses)))
->setCellValue("{$overall_col}10", "Overall Status")
->setCellValue("{$same_col}10", "Consistent")
->setCellValue("{$notes_col}10", "Notes")
->setCellValue("{$check_contents_col}10", "Check Contents");
$sheet->getStyle("{$overall_col}10:{$check_contents_col}10")
->getFont()
->setBold(true);
if (!FLATTEN) {
$sheet->getColumnDimension($overall_col)->setVisible(false);
$sheet->getColumnDimension($same_col)->setVisible(false);
}
if (WRAP_TEXT) {
$sheet->getStyle("{$check_contents_col}11:{$check_contents_col}{$sheet->getHighestDataRow()}")
->getAlignment()->setWrapText(true);
$sheet->getStyle("E11:E{$sheet->getHighestDataRow()}")
->getAlignment()->setWrapText(true);
}
$sheet->setCellValue('C2', $open_cat_1)
->setCellValue('C3', $open_cat_2)
->setCellValue('C4', $open_cat_3)
->setCellValue('C5', $not_a_finding)
->setCellValue('C6', $not_applicable)
->setCellValue('C7', $not_reviewed);
}
/**
* Method to split a string into an array (by new line \n) and use array_unique to remove duplicate strings
*
* @param string $str
*
* @return string
*/
function deduplicateString($str)
{
$ret = null;
$ret = str_replace(["\\n", PHP_EOL], "\r", $str);
$ret = array_unique(explode("\r", $ret));
$ret = html_entity_decode(implode("\r", $ret));
return $ret;
}
/**
* Method to add a comment to a particular cell
*
* @param PhpOffice\PhpSpreadsheet\Worksheet\Worksheet $sheet
* @param string $cell
* @param string $note
*/
function add_cell_comment(&$sheet, $cell, $note)
{
$sheet->getActiveSheet()
->getComment($cell)
->setAuthor(CREATOR);
$commentRichText = $sheet->getActiveSheet()
->getComment($cell)
->getText()->createTextRun('Scanner Notes:');
$commentRichText->getFont()->setBold(true);
$sheet->getActiveSheet()
->getComment($cell)
->getText()->createTextRun("\r\n");
$sheet->getActiveSheet()
->getComment($cell)
->getText()->createTextRun($note);
}