├── README.md └── Code.gs /README.md: -------------------------------------------------------------------------------- 1 | README 2 | -------------------------------------------------------------------------------- /Code.gs: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * Maintain this script with github.com/gsa/marketplace-fedramp-gov-data 4 | * 5 | * In "Project Settings" (gear icon to left), maintain the script properties variable for the account publishing updates. 6 | * 7 | */ 8 | 9 | /** 10 | * In order to protect itself from humans adding/updating/reordering/removing sheets and their columns 11 | * this script validates each sheet and their columns before processing. The errSheet and errArray 12 | * help eliminate repetitive error messages. 13 | */ 14 | var errSheet = ""; 15 | var errArr = []; 16 | 17 | /** 18 | * Using a regular expression to scrub data into html class names (used to filter 19 | * list data on the /products, /agencies, and /assessors pages). Some strings contain 20 | * characters that are not allowed in class names. 21 | * 22 | * /[\W_]+/g: 23 | * / = regex change delimiter. 24 | * [] = Set of characters to match. 25 | * \W = Any "non-word" character, opposite of [^a-zA-z0-9_] 26 | * _ = Also match on _ to keep consistent. 27 | * + = Match previous character(s) one-many times. 28 | * /g = Global. Do for entire string (don't return after first occurance). 29 | */ 30 | const REGEX_NON_WORDS = /[\W_]+/g; 31 | const REGEX_EXCESSIVE_SPACE = /\s/g; 32 | 33 | /** 34 | * Using a regular expression to scrub eastern time zone dates 4am/5am because this spreadsheet 35 | * is in Eastern Time and dates are being created with times, causing them to be 36 | * displayed differently for west coast. 37 | * 2013-05-01T04:00:00.000Z would be 5/1 in Eastern Time (UTC-4), but 4/30 in Pacific Time (UTC-7) 38 | */ 39 | const REGEX_TIME_ZONE = /T0[0-9]:00:00.000Z/g; 40 | const REPLACEMENT_TIME_ZONE = "T20:00:00.000Z"; 41 | 42 | /** 43 | * Sheet names go here here. Add new sheet names and their columns to isValidSetup() function. 44 | */ 45 | const MASTER_AUTHORIZATION_STATUS_SHEET = "Master Authorization Status"; 46 | const MASTER_AGENCY_TAB_SHEET = "Master Agency Tab"; 47 | const MASTER_3PAO_LIST_SHEET = "Master 3PAO List"; 48 | const INITIAL_ATOS_SHEET = "Initial ATOs"; 49 | const REUSE_ATOS_SHEET = "Reuse ATOs"; 50 | const METRICS_SHEET = "Metrics"; 51 | 52 | /** 53 | * Arrays of sheet columns for each sheet above 54 | */ 55 | const MASTER_AUTHORIZATION_STATUS_HEADERS = ["FR ID#", "CSP", "CSO", "Service Model", "Authorization Path", "Deployment Model", "Impact Level", "UEI Number", "Current 3PAO ID", "Security Contact Email", "Sales Contact Email", "Small Business?", "Logo URL", "CSP Website", "CSO Description", "CSP Business Function", "Additional Information", "In Process Initial Authorization Agency ID", "In Process Initial Sub Agency ID", "Current Active Status?", "FR Ready Active?", "FRR Most Recent Date", "In Process JAB Review Active?", "In Process JAB Review Most Recent Date", "In Process Program Review Active?", "In Process Program Review Most Recent Date", "In Process Program Review 2 Active?", "In Process Program Review 2 Most Recent Date", "In Process Agency Review Active?", "In Process Agency Review Most Recent Date", "FedRAMP In Process PMO Review Active?", "FedRAMP In Process PMO Review Most Recent Date", "FedRAMP Authorized Active?", "Non-Recent Authorized Services", "Recently Updated Authorized Services", "Authorizations", "Reuse", "Agency Authorizations", "Reuse Agencies", "Leveraged Systems", "Annual Assessment"]; 56 | const MASTER_AGENCY_TAB_HEADERS = ["Agency ID", "Agency Name", "Sub Agency", "E-mail", "Logo URL", "Website", "Authorizations", "Authorizations Number", "Reuse", "Reuse Number", "In Process (Agency Review)", "In Process (FedRAMP Review)","In Process (JAB Review)"]; 57 | const MASTER_3PAO_LIST_HEADERS = ["3PAO ID#", "Cert #", "3PAO Name", "POC Name", "POC Email", "Date Applied", "A2LA Accreditation Date", "FedRAMP Accreditation Date", "Logo URL", "Year Company Founded", "Website URL", "Primary Office Locations", "Description of 3PAO Services", "Consulting Services?", "Description of Consulting Services", "Additional Cyber Frameworks Your Company Is Accredited to Perform", "Active?", "CSPs providing consulting service to", "Current Clients", "Products Assessing (Number)"]; 58 | const INITIAL_ATOS_HEADERS = ["FR ID#", "Initial Authorization Agency ID", "Agency Name (Keep Hidden)", "Sub Agency ID", "Sub Agency Name (Keep hidden)", "Actual Authorizing Agency (keep hidden)", "Actual Reusing Agency Name (keep hidden)", "Agency ATO Date", "Authorization Date", "ATO Expiration", "Annual Assessment Date", "Authorizing Official", "Agency POC Email(s)", "Active?", "Comments (include all AA change dates)", "Authorization Year", "Authorization Path", "Authorization Timeline (in Days)"]; 59 | const REUSE_ATOS_HEADERS = ["FR ID", "Agency (Keep Hidden)", "Agency ID #", "Sub Agency Name (Keep hidden)", "Sub Agency #", "Actual Reusing Agency Name (keep hidden)", "Actual Reusing Agency (keep hidden)", "Agency Authorization", "Authorization Logged Date", "ATO Expiration", "AO", "Agency POC Email(s)", "Active?"]; 60 | const METRICS_HEADERS = ["FR ID", "Reuse ATOs", "Total ATOs", "Indirect Reuse", "ATOs", "Direct Reuse", "Total ATOs", "Indirect Reuse", "Ready", "In Process", "Authorizations"]; 61 | 62 | 63 | /** 64 | * Constants helping the "Metrics" "latest" list. These constants are used to help eliminate unset dates. 65 | */ 66 | const NO_FRR = "No FRR Date"; 67 | const NO_JAB = "Not Active"; 68 | const NO_PROG = "Not Active"; 69 | const NO_AGENCY = "Not Active"; 70 | const NO_PMO = "Not In Process"; 71 | const NO_AUTH = "Not Authorized"; 72 | 73 | const STATUS_FRR = "Now FedRAMP Ready"; 74 | const STATUS_JAB = "Now in JAB Review"; 75 | const STATUS_PROG = "Now in Program Review"; 76 | const STATUS_AGENCY = "Now in Agency Review"; 77 | const STATUS_PMO = "Now in PMO Review"; 78 | const STATUS_AUTH = "Now Authorized"; 79 | 80 | // ----+----1----+----2----+----3----+----4----+----5----+----6----+----7----+----8 81 | const STRING_80_BYTES = " "; 82 | 83 | /***************************************************************************************************/ 84 | 85 | 86 | /** 87 | * GitHub personal access token (https://github.com/blog/1509-personal-api-tokens) 88 | */ 89 | const github = { 90 | 'owner': 'GSA', 91 | 'repo': 'marketplace-fedramp-gov-data', 92 | 'path': PropertiesService.getScriptProperties().getProperty('GIT_FILENAME'), 93 | 'branch': 'master', 94 | 'accessToken': PropertiesService.getScriptProperties().getProperty('GIT_ACCESS_TOKEN'), 95 | 'commitMessage': Utilities.formatString('Published on %s', Utilities.formatDate(new Date(), 'America/New_York', 'yyyy-MM-dd HH:mm:ss')) 96 | }; 97 | 98 | /***************************************************************************************************/ 99 | 100 | 101 | /** 102 | * 103 | */ 104 | function main() { 105 | 106 | var ss = SpreadsheetApp.getActiveSpreadsheet(); 107 | 108 | if(isValidSetup(ss) == false) { 109 | return; 110 | } 111 | 112 | // Create json, retrieve sha, update github 113 | updateGitHubRepo(getGitHubSha(), createJson(ss).replace(REGEX_TIME_ZONE,REPLACEMENT_TIME_ZONE)); 114 | } 115 | 116 | /***************************************************************************************************/ 117 | 118 | 119 | /** 120 | * 121 | * 122 | */ 123 | function createJson(ss) { 124 | 125 | l("Building JSON..."); 126 | 127 | var json = { 128 | meta: { 129 | last_change: Utilities.formatDate(new Date(), 'America/New_York', 'yyyy-MM-dd\'T\'HH:mm:ss.SSS\'Z\''), 130 | produced_by: "General Services Administration" 131 | }, 132 | data: { 133 | "Metrics": { 134 | ready: "", 135 | in_process: "", 136 | total: "", 137 | latest: [] 138 | }, 139 | "Filters": { 140 | "product": {}, 141 | "agency": {}, 142 | "assessor":{} 143 | }, 144 | "Products": [], 145 | "Agencies": [], 146 | "Assessors": [], 147 | "AtoMapping": [], 148 | "ReuseMapping": [] 149 | } 150 | } 151 | 152 | 153 | /** 154 | * Metrics 155 | * 156 | * Note: "latest" metric built as part of "products" 157 | * 158 | */ 159 | 160 | l(" Metrics"); 161 | initErrorCols(METRICS_SHEET); 162 | 163 | var metricsVals = ss.getSheetByName(METRICS_SHEET).getDataRange().getValues(); 164 | json.data.Metrics.ready = metricsVals[1][getCol(METRICS_HEADERS, "Ready")]; 165 | json.data.Metrics.in_process = metricsVals[1][getCol(METRICS_HEADERS, "In Process")]; 166 | json.data.Metrics.total = metricsVals[1][getCol(METRICS_HEADERS, "Authorizations")]; 167 | 168 | /***************************************************************************************************/ 169 | 170 | 171 | /** 172 | * Master Authorization Status 173 | */ 174 | 175 | var product = { 176 | 177 | filter_classes: String, 178 | 179 | id: String, 180 | name: String, 181 | csp: String, 182 | cso: String, 183 | logo: String, 184 | service_offering: String, 185 | status: String, 186 | authorization: String, 187 | reuse: String, 188 | 189 | ready_status: String, 190 | ready_date: String, 191 | ip_jab_status: String, 192 | ip_jab_date: String, 193 | ip_prog_status: String, 194 | ip_prog_date: String, 195 | ip_prog_date2: String, 196 | ip_agency_status: String, 197 | ip_agency_date: String, 198 | ip_pmo_status: String, 199 | ip_pmo_date: String, 200 | auth_date: String, 201 | 202 | auth_type: String, 203 | fedramp_ready: String, 204 | 205 | partnering_agency: String, 206 | 207 | fedramp_auth: String, 208 | annual_assessment: String, 209 | independent_assessor: String, 210 | service_model: [], 211 | deployment_model: String, 212 | impact_level: String, 213 | 214 | leveraged_systems: String, 215 | 216 | agency_authorizations: [], 217 | agency_reuse: [], 218 | 219 | service_desc: String, 220 | fedramp_msg: String, 221 | sales_email: String, 222 | security_email: String, 223 | website: String, 224 | uei: String, 225 | small_business: String, 226 | business_function: [], 227 | 228 | service_last_90: [], 229 | all_others: [] 230 | } 231 | 232 | l(" Products"); 233 | initErrorCols(MASTER_AUTHORIZATION_STATUS_SHEET); 234 | 235 | var workLatest; 236 | var workArr = []; 237 | 238 | var masterAuthVals = ss.getSheetByName(MASTER_AUTHORIZATION_STATUS_SHEET).getDataRange().getValues(); 239 | 240 | /** 241 | * Loop through values on this tab, filling fields. Special processing generally happens at the bottom of the loop. 242 | */ 243 | 244 | for(var i = 1; i < masterAuthVals.length; i++) { 245 | 246 | if(masterAuthVals[i][getCol(MASTER_AUTHORIZATION_STATUS_HEADERS, "FR ID#")] == "" || 247 | masterAuthVals[i][getCol(MASTER_AUTHORIZATION_STATUS_HEADERS, "CSP")] == "" || 248 | masterAuthVals[i][getCol(MASTER_AUTHORIZATION_STATUS_HEADERS, "Current Active Status?")] == "No Status Found") { 249 | continue; 250 | } 251 | 252 | product = {}; 253 | 254 | 255 | product.id = masterAuthVals[i][getCol(MASTER_AUTHORIZATION_STATUS_HEADERS, "FR ID#")]; 256 | product.name = masterAuthVals[i][getCol(MASTER_AUTHORIZATION_STATUS_HEADERS, "CSP")]; 257 | product.csp = product.name; // To utilize sort 258 | product.cso = masterAuthVals[i][getCol(MASTER_AUTHORIZATION_STATUS_HEADERS, "CSO")]; 259 | 260 | product.logo = masterAuthVals[i][getCol(MASTER_AUTHORIZATION_STATUS_HEADERS, "Logo URL")]; 261 | product.service_offering = masterAuthVals[i][getCol(MASTER_AUTHORIZATION_STATUS_HEADERS, "CSO")]; 262 | product.status = masterAuthVals[i][getCol(MASTER_AUTHORIZATION_STATUS_HEADERS, "Current Active Status?")]; 263 | product.authorization = masterAuthVals[i][getCol(MASTER_AUTHORIZATION_STATUS_HEADERS, "Authorizations")]; 264 | product.reuse = masterAuthVals[i][getCol(MASTER_AUTHORIZATION_STATUS_HEADERS, "Reuse")]; 265 | 266 | product.ready_status = masterAuthVals[i][getCol(MASTER_AUTHORIZATION_STATUS_HEADERS, "FR Ready Active?")]; 267 | product.ready_date = masterAuthVals[i][getCol(MASTER_AUTHORIZATION_STATUS_HEADERS, "FRR Most Recent Date")]; 268 | product.ip_jab_status = masterAuthVals[i][getCol(MASTER_AUTHORIZATION_STATUS_HEADERS, "In Process JAB Review Active?")]; 269 | product.ip_jab_date = masterAuthVals[i][getCol(MASTER_AUTHORIZATION_STATUS_HEADERS, "In Process JAB Review Most Recent Date")]; 270 | product.ip_prog_status = masterAuthVals[i][getCol(MASTER_AUTHORIZATION_STATUS_HEADERS, "In Process Program Review Active?")]; 271 | product.ip_prog_date = masterAuthVals[i][getCol(MASTER_AUTHORIZATION_STATUS_HEADERS, "In Process Program Review Most Recent Date")]; 272 | product.ip_prog_date2 = masterAuthVals[i][getCol(MASTER_AUTHORIZATION_STATUS_HEADERS, "In Process Program Review 2 Most Recent Date")]; 273 | product.ip_agency_status = masterAuthVals[i][getCol(MASTER_AUTHORIZATION_STATUS_HEADERS, "In Process Agency Review Active?")]; 274 | product.ip_agency_date = masterAuthVals[i][getCol(MASTER_AUTHORIZATION_STATUS_HEADERS, "In Process Agency Review Most Recent Date")]; 275 | product.ip_pmo_status = masterAuthVals[i][getCol(MASTER_AUTHORIZATION_STATUS_HEADERS, "FedRAMP In Process PMO Review Active?")]; 276 | product.ip_pmo_date = masterAuthVals[i][getCol(MASTER_AUTHORIZATION_STATUS_HEADERS, "FedRAMP In Process PMO Review Most Recent Date")]; 277 | product.auth_date = masterAuthVals[i][getCol(MASTER_AUTHORIZATION_STATUS_HEADERS, "FedRAMP Authorized Active?")]; 278 | 279 | product.auth_type = masterAuthVals[i][getCol(MASTER_AUTHORIZATION_STATUS_HEADERS, "Authorization Path")]; 280 | product.fedramp_ready = masterAuthVals[i][getCol(MASTER_AUTHORIZATION_STATUS_HEADERS, "FRR Most Recent Date")]; 281 | 282 | product.partnering_agency = masterAuthVals[i][getCol(MASTER_AUTHORIZATION_STATUS_HEADERS, "In Process Initial Authorization Agency ID")]; 283 | 284 | if(masterAuthVals[i][getCol(MASTER_AUTHORIZATION_STATUS_HEADERS, "In Process Initial Sub Agency ID")] != "" ) { 285 | product.partnering_agency = masterAuthVals[i][getCol(MASTER_AUTHORIZATION_STATUS_HEADERS, "In Process Initial Sub Agency ID")]; 286 | } 287 | 288 | product.fedramp_auth = masterAuthVals[i][getCol(MASTER_AUTHORIZATION_STATUS_HEADERS, "FedRAMP Authorized Active?")]; 289 | product.annual_assessment = masterAuthVals[i][getCol(MASTER_AUTHORIZATION_STATUS_HEADERS, "Annual Assessment")]; 290 | product.independent_assessor = masterAuthVals[i][getCol(MASTER_AUTHORIZATION_STATUS_HEADERS, "Current 3PAO ID")]; 291 | product.service_model = Array.from(new Set(masterAuthVals[i][getCol(MASTER_AUTHORIZATION_STATUS_HEADERS, "Service Model")].split("|"))).sort(); 292 | product.deployment_model = masterAuthVals[i][getCol(MASTER_AUTHORIZATION_STATUS_HEADERS, "Deployment Model")]; 293 | product.impact_level = masterAuthVals[i][getCol(MASTER_AUTHORIZATION_STATUS_HEADERS, "Impact Level")]; 294 | product.impact_level_number = getImpactLevelNumber(product.impact_level); 295 | 296 | // Build and sort before storing 297 | workArr = getItems(masterAuthVals, Array.from(new Set(masterAuthVals[i][getCol(MASTER_AUTHORIZATION_STATUS_HEADERS, "Leveraged Systems")].split("|")))); 298 | product.leveraged_systems = quickSortOnObjectCSP(workArr, 0, workArr.length-1); 299 | 300 | // Force array into Set() to dedup, then sort before storing 301 | product.agency_authorizations = Array.from(new Set(masterAuthVals[i][getCol(MASTER_AUTHORIZATION_STATUS_HEADERS, "Agency Authorizations")].split("|"))).sort(); 302 | 303 | // Force array into Set() to dedup, then sort before storing 304 | product.agency_reuse = Array.from(new Set(masterAuthVals[i][getCol(MASTER_AUTHORIZATION_STATUS_HEADERS, "Reuse Agencies")].split("|"))).sort(); 305 | 306 | product.service_desc = masterAuthVals[i][getCol(MASTER_AUTHORIZATION_STATUS_HEADERS, "CSO Description")]; 307 | product.fedramp_msg = masterAuthVals[i][getCol(MASTER_AUTHORIZATION_STATUS_HEADERS, "Additional Information")]; 308 | product.sales_email = masterAuthVals[i][getCol(MASTER_AUTHORIZATION_STATUS_HEADERS, "Sales Contact Email")]; 309 | product.security_email = masterAuthVals[i][getCol(MASTER_AUTHORIZATION_STATUS_HEADERS, "Security Contact Email")]; 310 | product.website = masterAuthVals[i][getCol(MASTER_AUTHORIZATION_STATUS_HEADERS, "CSP Website")]; 311 | product.uei = masterAuthVals[i][getCol(MASTER_AUTHORIZATION_STATUS_HEADERS, "UEI Number")]; 312 | product.small_business = getYesNo(masterAuthVals[i][getCol(MASTER_AUTHORIZATION_STATUS_HEADERS, "Small Business?")]); 313 | product.business_function = Array.from(new Set(masterAuthVals[i][getCol(MASTER_AUTHORIZATION_STATUS_HEADERS, "CSP Business Function")].split("|"))).sort(); 314 | 315 | product.service_last_90 = Array.from(new Set(masterAuthVals[i][getCol(MASTER_AUTHORIZATION_STATUS_HEADERS, "Recently Updated Authorized Services")].split("|"))).sort(); 316 | product.all_others = Array.from(new Set(masterAuthVals[i][getCol(MASTER_AUTHORIZATION_STATUS_HEADERS, "Non-Recent Authorized Services")].split("|"))).sort(); 317 | 318 | /** 319 | * Special processing for finding the 3 "latest" dates from all the below columns. 320 | */ 321 | workLatest = getLatest(product.id, product.logo, product.service_offering, 322 | masterAuthVals[i][getCol(MASTER_AUTHORIZATION_STATUS_HEADERS, "FRR Most Recent Date")], 323 | masterAuthVals[i][getCol(MASTER_AUTHORIZATION_STATUS_HEADERS, "In Process JAB Review Most Recent Date")], 324 | masterAuthVals[i][getCol(MASTER_AUTHORIZATION_STATUS_HEADERS, "In Process Program Review Most Recent Date")], 325 | masterAuthVals[i][getCol(MASTER_AUTHORIZATION_STATUS_HEADERS, "In Process Agency Review Most Recent Date")], 326 | masterAuthVals[i][getCol(MASTER_AUTHORIZATION_STATUS_HEADERS, "FedRAMP In Process PMO Review Most Recent Date")], 327 | masterAuthVals[i][getCol(MASTER_AUTHORIZATION_STATUS_HEADERS, "FedRAMP Authorized Active?")]); 328 | 329 | if(workLatest.date != "") { // If there's a good date in any of the above columns... 330 | 331 | json.data.Metrics.latest.push(workLatest); // Build array of "latest" objects to be paired down later. 332 | } 333 | 334 | // Filter classes 335 | product.filter_classes = ""; 336 | product.filter_classes += " filter-status-" + product.status.replace(REGEX_NON_WORDS,"-"); 337 | product.filter_classes += " filter-impact-level-" + product.impact_level.replace(REGEX_NON_WORDS,"-"); 338 | product.filter_classes += " filter-auth-type-" + product.auth_type.replace(REGEX_NON_WORDS,"-"); 339 | product.filter_classes += " filter-deployment-model-" + product.deployment_model.replace(REGEX_NON_WORDS,"-"); 340 | product.filter_classes += " filter-small-business-" + product.small_business.replace(REGEX_NON_WORDS,"-"); 341 | product.filter_classes += " filter-assessor-" + product.independent_assessor.replace(REGEX_NON_WORDS,"-"); 342 | 343 | for(var j = 0; j < product.service_model.length; j++) { 344 | product.filter_classes += " filter-service-model-" + product.service_model[j].replace(REGEX_NON_WORDS,"-"); 345 | } 346 | 347 | for(var j = 0; j < product.business_function.length; j++) { 348 | product.filter_classes += " filter-business-function-" + product.business_function[j].replace(REGEX_NON_WORDS,"-"); 349 | } 350 | 351 | json.data.Products.push(product); // Build array of Products 352 | } 353 | 354 | json.data.Products = quickSortOnObjectCSP(json.data.Products, 0, json.data.Products.length-1); // Sort 355 | 356 | /** 357 | * This object should ALWAYS contain more than 3 items before the splice(). If this 358 | * line blows up, there are bigger problems. 359 | */ 360 | json.data.Metrics.latest = quickSortOnObjectDate(json.data.Metrics.latest, 0, json.data.Metrics.latest.length-1).splice(0,3); 361 | 362 | /***************************************************************************************************/ 363 | 364 | 365 | /** 366 | * Agencies 367 | */ 368 | 369 | var agency = { 370 | 371 | filter_classes: String, 372 | 373 | id: String, 374 | parent: String, 375 | sub_id: String, 376 | sub: String, 377 | csp: String, 378 | logo: String, 379 | authorization: Number, 380 | reuse: Number, 381 | email: String, 382 | website: String, 383 | auths: [], 384 | reuses: [], 385 | procs: [] 386 | } 387 | 388 | l(" Agencies"); 389 | initErrorCols(MASTER_AGENCY_TAB_SHEET); 390 | 391 | var masterAgencyVals = ss.getSheetByName(MASTER_AGENCY_TAB_SHEET).getDataRange().getValues(); 392 | 393 | /** 394 | * Loop through values on this tab, filling fields. Special processing generally happens at the bottom of the loop. 395 | */ 396 | 397 | for(var i = 1; i < masterAgencyVals.length; i++) { 398 | 399 | if(masterAgencyVals[i][getCol(MASTER_AGENCY_TAB_HEADERS,"Agency ID")] == "" 400 | || masterAgencyVals[i][getCol(MASTER_AGENCY_TAB_HEADERS,"Agency Name")] == "" 401 | || masterAgencyVals[i][getCol(MASTER_AGENCY_TAB_HEADERS,"Authorizations Number")] == "") { 402 | continue; 403 | } 404 | 405 | agency = {}; 406 | 407 | agency.id = masterAgencyVals[i][getCol(MASTER_AGENCY_TAB_HEADERS,"Agency ID")]; 408 | agency.parent = masterAgencyVals[i][getCol(MASTER_AGENCY_TAB_HEADERS, "Agency Name")]; 409 | agency.sub = masterAgencyVals[i][getCol(MASTER_AGENCY_TAB_HEADERS, "Sub Agency")]; 410 | 411 | if(agency.parent == agency.sub) { // Sub and Parent the same? 412 | agency.sub = ""; // Remove. We don't want it displayed twice on the webpage. 413 | } 414 | 415 | if(agency.sub != "") { 416 | agency.sub_id = agency.id; 417 | } 418 | 419 | agency.csp = concatParentSub(agency.parent, agency.sub); 420 | 421 | agency.logo = masterAgencyVals[i][getCol(MASTER_AGENCY_TAB_HEADERS, "Logo URL")]; 422 | agency.authorization = masterAgencyVals[i][getCol(MASTER_AGENCY_TAB_HEADERS, "Authorizations Number")]; 423 | agency.reuse = masterAgencyVals[i][getCol(MASTER_AGENCY_TAB_HEADERS, "Reuse Number")]; 424 | agency.email = masterAgencyVals[i][getCol(MASTER_AGENCY_TAB_HEADERS, "E-mail")]; 425 | agency.website = masterAgencyVals[i][getCol(MASTER_AGENCY_TAB_HEADERS, "Website")]; 426 | 427 | // Build and sort before storing 428 | workArr = getItems(masterAuthVals, Array.from(new Set(masterAgencyVals[i][getCol(MASTER_AGENCY_TAB_HEADERS, "Authorizations")].split("|")))); 429 | agency.auths = quickSortOnObjectCSP(workArr, 0, workArr.length-1); 430 | 431 | // Build and sort before storing 432 | workArr = getItems(masterAuthVals, Array.from(new Set(masterAgencyVals[i][getCol(MASTER_AGENCY_TAB_HEADERS, "Reuse")].split("|")))); 433 | agency.reuses = quickSortOnObjectCSP(workArr, 0, workArr.length-1); 434 | 435 | // Build and sort before storing. There's a concat snuck in here, too. 436 | workArr = getItems(masterAuthVals, Array.from(new Set(masterAgencyVals[i][getCol(MASTER_AGENCY_TAB_HEADERS, "In Process (Agency Review)")].split("|"))), "Agency Review").concat(getItems(masterAuthVals, Array.from(new Set(masterAgencyVals[i][getCol(MASTER_AGENCY_TAB_HEADERS, "In Process (FedRAMP Review)")].split("|"))),"FedRAMP Review"), getItems(masterAuthVals, Array.from(new Set(masterAgencyVals[i][getCol(MASTER_AGENCY_TAB_HEADERS, "In Process (JAB Review)")].split("|"))),"JAB Review")); 437 | agency.procs = quickSortOnObjectCSP(workArr, 0, workArr.length-1); 438 | 439 | 440 | // Filter classes 441 | agency.filter_classes = ""; 442 | agency.filter_classes += " filter-parent-agency-" + agency.parent.replace(REGEX_NON_WORDS,"-"); 443 | agency.filter_classes += getFilterClassBucket("authorization", agency.authorization); 444 | agency.filter_classes += getFilterClassBucket("reuse", agency.reuse); 445 | agency.filter_classes += getFilterClassImpactAndOffering(Array.from(new Set(agency.auths.concat(agency.procs)))); 446 | 447 | json.data.Agencies.push(agency); // Build array of Agencies 448 | } 449 | 450 | json.data.Agencies = quickSortOnObjectCSP(json.data.Agencies, 0, json.data.Agencies.length-1); 451 | 452 | /***************************************************************************************************/ 453 | 454 | 455 | /** 456 | * Assessors 457 | */ 458 | 459 | var assessor = { 460 | 461 | filter_classes: String, 462 | 463 | id: String, 464 | name: String, 465 | csp: String, 466 | logo: String, 467 | products_assessing: Number, 468 | accredited_since: String, 469 | poc: String, 470 | email: String, 471 | founded: String, 472 | address: String, 473 | desc: String, 474 | services: String, 475 | csps: String, 476 | frameworks: String, 477 | clients: [] 478 | } 479 | 480 | l(" Assessors"); 481 | initErrorCols(MASTER_3PAO_LIST_SHEET); 482 | 483 | var master3paoVals = ss.getSheetByName(MASTER_3PAO_LIST_SHEET).getDataRange().getValues(); 484 | 485 | /** 486 | * Loop through values on this tab, filling fields. Special processing generally happens at the bottom of the loop. 487 | */ 488 | for(var i = 1; i < master3paoVals.length; i++) { 489 | 490 | if(master3paoVals[i][getCol(MASTER_3PAO_LIST_HEADERS, "3PAO ID#")] == "") { 491 | continue; 492 | } 493 | 494 | assessor = {}; 495 | 496 | assessor.id = master3paoVals[i][getCol(MASTER_3PAO_LIST_HEADERS, "3PAO ID#")]; 497 | assessor.name = master3paoVals[i][getCol(MASTER_3PAO_LIST_HEADERS, "3PAO Name")]; 498 | assessor.csp = assessor.name; 499 | 500 | assessor.logo = master3paoVals[i][getCol(MASTER_3PAO_LIST_HEADERS, "Logo URL")]; 501 | assessor.products_assessing = master3paoVals[i][getCol(MASTER_3PAO_LIST_HEADERS, "Products Assessing (Number)")]; 502 | 503 | assessor.accredited_since = master3paoVals[i][getCol(MASTER_3PAO_LIST_HEADERS, "FedRAMP Accreditation Date")]; 504 | assessor.poc = master3paoVals[i][getCol(MASTER_3PAO_LIST_HEADERS, "POC Name")]; 505 | assessor.email = master3paoVals[i][getCol(MASTER_3PAO_LIST_HEADERS, "POC Email")]; 506 | assessor.founded = master3paoVals[i][getCol(MASTER_3PAO_LIST_HEADERS, "Year Company Founded")]; 507 | assessor.address = master3paoVals[i][getCol(MASTER_3PAO_LIST_HEADERS, "Primary Office Locations")]; 508 | assessor.desc = master3paoVals[i][getCol(MASTER_3PAO_LIST_HEADERS, "Description of 3PAO Services")]; 509 | 510 | if(master3paoVals[i][getCol(MASTER_3PAO_LIST_HEADERS, "Consulting Services?")] == 'Y') { 511 | 512 | assessor.services = master3paoVals[i][getCol(MASTER_3PAO_LIST_HEADERS, "Description of Consulting Services")]; 513 | } 514 | 515 | assessor.csps = master3paoVals[i][getCol(MASTER_3PAO_LIST_HEADERS, "CSPs providing consulting service to")]; 516 | assessor.frameworks = master3paoVals[i][getCol(MASTER_3PAO_LIST_HEADERS, "Additional Cyber Frameworks Your Company Is Accredited to Perform")]; 517 | 518 | // Build and sort before storing 519 | workArr = getItems(masterAuthVals, Array.from(new Set(master3paoVals[i][getCol(MASTER_3PAO_LIST_HEADERS, "Current Clients")].split("|")))); 520 | assessor.clients = quickSortOnObjectCSP(workArr, 0, workArr.length-1); 521 | 522 | // Filter classes 523 | assessor.filter_classes = ""; 524 | assessor.filter_classes += getFilterClassBucket("products-assessing", assessor.products_assessing); 525 | assessor.filter_classes += getFilterClassImpactAndOffering(assessor.clients); 526 | 527 | json.data.Assessors.push(assessor); // Build array of Assessors 528 | } 529 | 530 | json.data.Assessors = quickSortOnObjectCSP(json.data.Assessors, 0, json.data.Assessors.length-1); 531 | 532 | /***************************************************************************************************/ 533 | 534 | /** 535 | * Product Filters 536 | */ 537 | 538 | l(" Product Filters"); 539 | 540 | var filter = { 541 | name: String, 542 | class_name: String 543 | } 544 | var productFilters = { 545 | status: [], 546 | business_function: [], 547 | service_model: [], 548 | impact_level: [], 549 | auth_type: [], 550 | deployment_models: [], 551 | small_business: [], 552 | assessor: [] 553 | } 554 | 555 | productFilters.status = getFilter("status", masterAuthVals, MASTER_AUTHORIZATION_STATUS_HEADERS, "Current Active Status?"); 556 | productFilters.business_function = getFilter("business-function", masterAuthVals, MASTER_AUTHORIZATION_STATUS_HEADERS, "CSP Business Function"); 557 | productFilters.service_model = getFilter("service-model", masterAuthVals, MASTER_AUTHORIZATION_STATUS_HEADERS, "Service Model"); 558 | 559 | productFilters.impact_level.push({ name: "LI-SaaS", class_name: "filter-impact-level-LI-SaaS"}); 560 | productFilters.impact_level.push({ name: "Low", class_name: "filter-impact-level-Low"}); 561 | productFilters.impact_level.push({ name: "Moderate",class_name: "filter-impact-level-Moderate"}); 562 | productFilters.impact_level.push({ name: "High", class_name: "filter-impact-level-High"}); 563 | 564 | productFilters.auth_type = getFilter("auth-type", masterAuthVals, MASTER_AUTHORIZATION_STATUS_HEADERS, "Authorization Path"); 565 | productFilters.auth_type.splice(productFilters.auth_type.indexOf("Program"),1); 566 | productFilters.deployment_models = getFilter("deployment-model", masterAuthVals, MASTER_AUTHORIZATION_STATUS_HEADERS, "Deployment Model"); 567 | 568 | // productFilters.small_business.push({ name: "Yes", class_name: "filter-small-business-Yes"}); 569 | // productFilters.small_business.push({ name: "No", class_name: "filter-small-business-No"}); 570 | 571 | productFilters.assessor = getFilter("assessor", masterAuthVals, MASTER_AUTHORIZATION_STATUS_HEADERS, "Current 3PAO ID"); 572 | 573 | json.data.Filters.product = productFilters; 574 | 575 | /***************************************************************************************************/ 576 | 577 | 578 | /** 579 | * Agency Filters 580 | */ 581 | 582 | l(" Agency Filters"); 583 | 584 | var agencyFilters = { 585 | parent_agency: [], 586 | authorization: [], 587 | reuse: [], 588 | impact_level: [] 589 | } 590 | 591 | agencyFilters.parent_agency = getFilter("parent-agency", masterAgencyVals, MASTER_AGENCY_TAB_HEADERS, "Agency Name"); 592 | 593 | // if these numbers change, also change logic in getFilterClassBucket 594 | agencyFilters.authorization.push({ name: "0-5", class_name: "filter-authorization-1"}); 595 | agencyFilters.authorization.push({ name: "6-10", class_name: "filter-authorization-2"}); 596 | agencyFilters.authorization.push({ name: "11-20", class_name: "filter-authorization-3"}); 597 | agencyFilters.authorization.push({ name: "21+", class_name: "filter-authorization-4"}); 598 | 599 | 600 | agencyFilters.reuse.push({ name: "0-5", class_name: "filter-reuse-1"}); 601 | agencyFilters.reuse.push({ name: "6-10", class_name: "filter-reuse-2"}); 602 | agencyFilters.reuse.push({ name: "11-20", class_name: "filter-reuse-3"}); 603 | agencyFilters.reuse.push({ name: "21+", class_name: "filter-reuse-4"}); 604 | 605 | 606 | agencyFilters.impact_level.push({ name: "LI-SaaS", class_name: "filter-impact-level-LI-SaaS"}); 607 | agencyFilters.impact_level.push({ name: "Low", class_name: "filter-impact-level-Low"}); 608 | agencyFilters.impact_level.push({ name: "Moderate",class_name: "filter-impact-level-Moderate"}); 609 | agencyFilters.impact_level.push({ name: "High", class_name: "filter-impact-level-High"}); 610 | 611 | json.data.Filters.agency = agencyFilters; 612 | 613 | /***************************************************************************************************/ 614 | 615 | 616 | /** 617 | * Assessor Filters 618 | */ 619 | 620 | l(" Assessor Filters"); 621 | 622 | var assessorFilters = { 623 | product_assessing: [], 624 | impact_level: [], 625 | status: [] 626 | } 627 | 628 | assessorFilters.product_assessing.push({ name: "0-5", class_name: "filter-products-assessing-1"}); 629 | assessorFilters.product_assessing.push({ name: "6-10", class_name: "filter-products-assessing-2"}); 630 | assessorFilters.product_assessing.push({ name: "11-20", class_name: "filter-products-assessing-3"}); 631 | assessorFilters.product_assessing.push({ name: "21+", class_name: "filter-products-assessing-4"}); 632 | 633 | assessorFilters.impact_level = json.data.Filters.product.impact_level; 634 | assessorFilters.status = json.data.Filters.product.status; 635 | 636 | json.data.Filters.assessor = assessorFilters; 637 | 638 | /***************************************************************************************************/ 639 | 640 | /** 641 | * AtoMapping 642 | */ 643 | 644 | var atoRec = { 645 | id: "", 646 | agency_id: "", 647 | parent: "", 648 | sub: "", 649 | ato_date: "", 650 | auth_date: "", 651 | exp_date: "", 652 | assessment_date: "" 653 | } 654 | 655 | l(" Initial ATOs"); 656 | initErrorCols(INITIAL_ATOS_SHEET); 657 | 658 | var initialAtoVals = ss.getSheetByName(INITIAL_ATOS_SHEET).getDataRange().getValues(); 659 | 660 | for(var i = 1; i < initialAtoVals.length; i++) { 661 | 662 | if(initialAtoVals[i][getCol(INITIAL_ATOS_HEADERS, "Active?")] != "Y") { 663 | continue; 664 | } 665 | 666 | atoRec = {}; 667 | 668 | atoRec.id = initialAtoVals[i][getCol(INITIAL_ATOS_HEADERS, "FR ID#")]; 669 | atoRec.agency_id = initialAtoVals[i][getCol(INITIAL_ATOS_HEADERS, "Actual Authorizing Agency (keep hidden)")]; 670 | atoRec.parent = initialAtoVals[i][getCol(INITIAL_ATOS_HEADERS, "Agency Name (Keep Hidden)")]; 671 | atoRec.sub = initialAtoVals[i][getCol(INITIAL_ATOS_HEADERS, "Sub Agency Name (Keep hidden)")]; 672 | atoRec.ato_date = initialAtoVals[i][getCol(INITIAL_ATOS_HEADERS, "Agency ATO Date")]; 673 | atoRec.auth_date = initialAtoVals[i][getCol(INITIAL_ATOS_HEADERS, "Authorization Date")]; 674 | atoRec.exp_date = initialAtoVals[i][getCol(INITIAL_ATOS_HEADERS, "ATO Expiration")]; 675 | atoRec.assessment_date = initialAtoVals[i][getCol(INITIAL_ATOS_HEADERS, "Annual Assessment Date")]; 676 | 677 | json.data.AtoMapping.push(atoRec); // Build array of Assessors 678 | } 679 | 680 | json.data.AtoMapping = quickSortOnObjectId(json.data.AtoMapping, 0, json.data.AtoMapping.length-1); 681 | 682 | /***************************************************************************************************/ 683 | 684 | 685 | /** 686 | * reuseMapping 687 | */ 688 | 689 | var reuseRec = { 690 | id: "", 691 | agency_id: "", 692 | parent: "", 693 | sub_id: "", 694 | sub: "", 695 | ato_date: "", 696 | auth_date: "", 697 | exp_date: "" 698 | } 699 | 700 | l(" Reuse ATOs"); 701 | initErrorCols(REUSE_ATOS_SHEET); 702 | 703 | var reuseVals = ss.getSheetByName(REUSE_ATOS_SHEET).getDataRange().getValues(); 704 | 705 | for(var i = 1; i < reuseVals.length; i++) { 706 | 707 | if(reuseVals[i][getCol(REUSE_ATOS_HEADERS, "Active?")] != "Y") { 708 | continue; 709 | } 710 | 711 | reuseRec = {}; 712 | 713 | reuseRec.id = reuseVals[i][getCol(REUSE_ATOS_HEADERS, "FR ID")]; 714 | reuseRec.agency_id = reuseVals[i][getCol(REUSE_ATOS_HEADERS, "Agency ID #")]; 715 | reuseRec.parent = reuseVals[i][getCol(REUSE_ATOS_HEADERS, "Agency (Keep Hidden)")]; 716 | reuseRec.sub_id = reuseVals[i][getCol(REUSE_ATOS_HEADERS, "Sub Agency #")]; 717 | reuseRec.sub = reuseVals[i][getCol(REUSE_ATOS_HEADERS, "Sub Agency Name (Keep hidden)")]; 718 | reuseRec.ato_date = reuseVals[i][getCol(REUSE_ATOS_HEADERS, "Agency Authorization")]; 719 | reuseRec.auth_date = reuseVals[i][getCol(REUSE_ATOS_HEADERS, "Authorization Logged Date")]; 720 | reuseRec.exp_date = reuseVals[i][getCol(REUSE_ATOS_HEADERS, "ATO Expiration")]; 721 | 722 | json.data.ReuseMapping.push(reuseRec); // Build array of Assessors 723 | } 724 | 725 | json.data.ReuseMapping = quickSortOnObjectId(json.data.ReuseMapping, 0, json.data.ReuseMapping.length-1); 726 | 727 | /***************************************************************************************************/ 728 | 729 | return JSON.stringify(json, null); 730 | 731 | } 732 | 733 | /***************************************************************************************************/ 734 | 735 | 736 | /** 737 | * Find the latest date from the last 5 fields passed in (if any of them have dates). 738 | * 739 | * @param {frr} - Possible date (FedRAMP Ready) 740 | * @param {jab} - Possible date (JAB Review) 741 | * @param {agency} - Possible date (Agency Review) 742 | * @param {pmo} - Possible date (PMO Review) 743 | * @param {auth} - Possible date (Auth Review) 744 | * @returns {Object} - Latest date and its "status" (description of date) 745 | */ 746 | function getLatest(id, logo, cso, frr, jab, prog, agency, pmo, auth) { 747 | 748 | var rec = { 749 | id: String, 750 | logo: String, 751 | cso: String, 752 | date: String, 753 | status: String 754 | } 755 | 756 | // No good dates? Return. 757 | if(frr == NO_FRR && jab == NO_JAB && prog == NO_PROG && agency == NO_AGENCY && pmo == NO_PMO && auth == NO_AUTH) { 758 | 759 | return rec; 760 | } 761 | 762 | rec.id = id; 763 | rec.logo = logo; 764 | rec.cso = cso; 765 | 766 | // Find latest date 767 | var item = maxDate(maxDate(maxDate(maxDate(maxDate( 768 | {date: scrubNo(frr), status: STATUS_FRR}, 769 | {date: scrubNo(jab), status: STATUS_JAB}), 770 | {date: scrubNo(prog), status: STATUS_PROG}), 771 | {date: scrubNo(agency), status: STATUS_AGENCY}), 772 | {date: scrubNo(pmo), status: STATUS_PMO}), 773 | {date: scrubNo(auth), status: STATUS_AUTH}); 774 | 775 | rec.date = item.date; 776 | rec.status = item.status; 777 | 778 | return rec; 779 | } 780 | 781 | /***************************************************************************************************/ 782 | 783 | 784 | /** 785 | * This utility function is used to clear any default literals within a date field. 786 | * 787 | * @param {inDate} - Possible detault literal or actual date. 788 | * @returns {String} - Empty or the date passed in. 789 | */ 790 | function scrubNo(inDate) { 791 | 792 | if(inDate == NO_FRR || inDate == NO_JAB || inDate == NO_PROG || inDate == NO_AGENCY || inDate == NO_PMO || inDate == NO_AUTH) { 793 | 794 | return ""; 795 | } 796 | return inDate; 797 | } 798 | 799 | /***************************************************************************************************/ 800 | 801 | 802 | /** 803 | * Returns the greater date between two Objects containing a .date variable. 804 | * 805 | * @param {a} - Object of Date and Status (date description) 806 | * @param {b} - Object of Date and Status (date description) 807 | * @returns {Object} - Date and Status of "latest" date 808 | */ 809 | function maxDate(a, b) { 810 | 811 | if(a.date > b.date) { 812 | 813 | return a; 814 | } 815 | return b; 816 | } 817 | 818 | /***************************************************************************************************/ 819 | 820 | 821 | /** 822 | * Get unique sorted list of all values present in a column. 823 | * 824 | * @param {inLabel} - Label to be applied to the filter so each category is unique. 825 | * @param {inVals} - Values from a sheet 826 | * @param {inHeaders} - Array of headers from a sheet 827 | * @param {inCol} - Column 828 | * @returns {Object} - 829 | */ 830 | function getFilter(inLabel, inVals, inHeaders, inCol) { 831 | 832 | var filter = { 833 | name: String, 834 | class_name: String 835 | } 836 | 837 | var filterArr = []; 838 | 839 | for(var i = 1; i < inVals.length; i++) { 840 | 841 | filterArr = filterArr.concat(inVals[i][getCol(inHeaders, inCol)].split("|")); // Some columns are pipe-delimited 842 | } 843 | 844 | // The easiest way to dedup an array is to force it into a Set 845 | filterArr = Array.from(new Set(filterArr)).sort(); 846 | 847 | var filters = []; 848 | 849 | for(var i = 0; i < filterArr.length; i++) { 850 | 851 | filter = {}; 852 | 853 | filter.name = filterArr[i]; 854 | filter.class_name = "filter-" + inLabel + "-" + filterArr[i].replace(REGEX_NON_WORDS, "-"); 855 | 856 | if (filter.name != "" && filterArr[i] != "No Status Found") { 857 | filters.push(filter); 858 | } 859 | } 860 | 861 | return filters; 862 | 863 | } 864 | 865 | /***************************************************************************************************/ 866 | 867 | 868 | /** 869 | * This function initializes the error sheet name and array to 870 | * the sheet name about to be processed. The array will ultimately hold 871 | * unique sheet/col info so we don't get hundreds of identical messages 872 | * about columns missing. 873 | * 874 | * @param {inSheet} - active spreadsheet being processed 875 | */ 876 | function initErrorCols(inSheet) { 877 | 878 | errSheet = inSheet; 879 | errArr = []; 880 | } 881 | 882 | /***************************************************************************************************/ 883 | 884 | 885 | /** 886 | * Prints a unique error message and adds it to an array so 887 | * it won't be duplicated. This uses the global sheet name 888 | * set in initErrorCols() above. 889 | * 890 | * @param {inCol} - column not found 891 | */ 892 | function addErrorCol(inCol) { 893 | 894 | var errMsg = "Sheet<" + errSheet + "> Col<" + inCol + ">"; 895 | 896 | if(errArr.indexOf(errMsg) == -1) { // Unique message? 897 | 898 | l(errMsg); // Print it 899 | errArr.push(errMsg); // Add it to the error array so a future duplicate won't be printed. 900 | } 901 | } 902 | 903 | /***************************************************************************************************/ 904 | 905 | 906 | /** 907 | * Attempt to find a column within an array of header names. 908 | * 909 | * @param {inHeaders} - Array of header names 910 | * @param {inCol} - index of column to find 911 | * @returns {Integer} - index of column found in array of headers. -1 if not found. 912 | */ 913 | function getCol(inHeaders, inCol) { 914 | 915 | var colNum = inHeaders.indexOf(inCol); // Find index of column in header array 916 | 917 | if(colNum == -1) { // Not found? 918 | 919 | addErrorCol(inCol); // Print unique missing columns 920 | } 921 | return colNum; // Return index 922 | } 923 | 924 | /***************************************************************************************************/ 925 | 926 | 927 | /** 928 | * In several locations of the AuthLog cells contain a pipe-delimited list of 929 | * IDs that are used to create an array of objects containing info from 930 | * corresponding entries on the Master Authorization Status sheet. 931 | * 932 | * @param {masterAuthVals} - 2D array of values from the Master Authorization Status sheet so they don't have to be retrieved multiple times. 933 | * @param {itemArray} - Array of Master Authorization Status IDs 934 | * @returns {Array of Objects} - Array of "item" objects 935 | */ 936 | function getItems(masterAuthVals, itemArray, inConst = "") { 937 | 938 | var item = { 939 | id: String, 940 | csp: String, 941 | cso: String, 942 | status: String, 943 | impact: String 944 | } 945 | 946 | var list = []; 947 | 948 | /** 949 | * Loop through each ID from the array 950 | */ 951 | for(var j = 0; j < itemArray.length; j++) { 952 | 953 | /** 954 | * The itemArray is created from a string being split(). The result will 955 | * be at least 1 item (even if it's splitting a null field). Check for blank before 956 | * looping through everything. 957 | */ 958 | if(itemArray[j] != "") { 959 | 960 | /** 961 | * Loop through the Master Authorization Status rows, looking for the ID. 962 | */ 963 | for(var k = 1; k < masterAuthVals.length; k++) { 964 | 965 | /** 966 | * If ID matches, push an item onto the array 967 | */ 968 | if(itemArray[j] == masterAuthVals[k][getCol(MASTER_AUTHORIZATION_STATUS_HEADERS, "FR ID#")] && 969 | masterAuthVals[k][getCol(MASTER_AUTHORIZATION_STATUS_HEADERS, "Current Active Status?")] != "No Status Found") { 970 | 971 | item = {} 972 | item.id = masterAuthVals[k][getCol(MASTER_AUTHORIZATION_STATUS_HEADERS, "FR ID#")]; 973 | item.csp = masterAuthVals[k][getCol(MASTER_AUTHORIZATION_STATUS_HEADERS, "CSP")]; 974 | item.cso = masterAuthVals[k][getCol(MASTER_AUTHORIZATION_STATUS_HEADERS, "CSO")]; 975 | 976 | if(inConst != "") { 977 | item.status = inConst; 978 | } else { 979 | item.status = masterAuthVals[k][getCol(MASTER_AUTHORIZATION_STATUS_HEADERS, "Current Active Status?")]; 980 | } 981 | 982 | item.impact_level = masterAuthVals[k][getCol(MASTER_AUTHORIZATION_STATUS_HEADERS, "Impact Level")]; 983 | item.impact_level_number = getImpactLevelNumber(item.impact_level); 984 | 985 | list.push(item); 986 | 987 | break; 988 | } 989 | } 990 | } 991 | } 992 | return list; 993 | } 994 | 995 | /***************************************************************************************************/ 996 | 997 | 998 | /** 999 | * Individually validates each sheet and their columns 1000 | * 1001 | * @param {ss} - active spreadsheet 1002 | * @returns {Boolean} - false if any sheet or sheet's column is missing. 1003 | */ 1004 | function isValidSetup(ss) { 1005 | 1006 | l("Validating Sheets and Columns"); 1007 | 1008 | if(isValidSheetColumns(ss, MASTER_AUTHORIZATION_STATUS_SHEET, MASTER_AUTHORIZATION_STATUS_HEADERS) == false 1009 | || isValidSheetColumns(ss, MASTER_AGENCY_TAB_SHEET, MASTER_AGENCY_TAB_HEADERS) == false 1010 | || isValidSheetColumns(ss, MASTER_3PAO_LIST_SHEET, MASTER_3PAO_LIST_HEADERS) == false 1011 | || isValidSheetColumns(ss, METRICS_SHEET, METRICS_HEADERS) == false 1012 | || isValidSheetColumns(ss, INITIAL_ATOS_SHEET, INITIAL_ATOS_HEADERS) == false 1013 | || isValidSheetColumns(ss, REUSE_ATOS_SHEET, REUSE_ATOS_HEADERS) == false) { 1014 | 1015 | return false; 1016 | } 1017 | return true; 1018 | } 1019 | 1020 | /***************************************************************************************************/ 1021 | 1022 | 1023 | /** 1024 | * Validates columns expected in a sheet 1025 | * 1026 | * @param {ss} - active spreadsheet 1027 | * @param {inName} - name of sheet to validate 1028 | * @param {inHeaders} - array of headers expected for sheet 1029 | * @returns {Boolean} - false if sheet or sheet's column is missing. 1030 | */ 1031 | function isValidSheetColumns(ss,inName,inHeaders) { 1032 | 1033 | if(ss.getSheetByName(inName) == false) { // Does the sheet exist? 1034 | 1035 | l("Missing Sheet Name: <"+ inName +">"); // Print something useful 1036 | return false; // Bail 1037 | } 1038 | 1039 | // Grab headers from the actual sheet to validate 1040 | var range = ss.getSheetByName(inName).getRange(1,1,1,inHeaders.length); 1041 | var values = range.getValues(); 1042 | 1043 | // Compare actual headers to the headers we expect. 1044 | for(var i = 0; i < inHeaders.length; i++) { 1045 | 1046 | if(values[0][i] != inHeaders[i]) { 1047 | 1048 | l("Invalid Header\n Sheet<" + inName +">\n Change<" + inHeaders[i] +"> --> <"+ values[0][i]+">"); 1049 | return false; // Print something useful and bail. 1050 | } 1051 | } 1052 | return true; 1053 | } 1054 | 1055 | /***************************************************************************************************/ 1056 | 1057 | 1058 | /** 1059 | * Lazy way to print log messages... OR GENIUS!? 1060 | * 1061 | * @param {inMsg} - message to possibly print to log 1062 | */ 1063 | function l(inMsg) { 1064 | Logger.log(inMsg); 1065 | } 1066 | 1067 | /***************************************************************************************************/ 1068 | 1069 | 1070 | /** 1071 | * HTTP PUT to commit an element to github. 1072 | * 1073 | * @param {sha} - sha of element being replaced 1074 | * @param {json} - json file contents to commit to 1075 | */ 1076 | function updateGitHubRepo(sha, json) { 1077 | 1078 | l("Updating GitHub"); 1079 | 1080 | var requestUrl = Utilities.formatString( 1081 | 'https://api.github.com/repos/%s/%s/contents/%s', 1082 | github.owner, 1083 | github.repo, 1084 | github.path, 1085 | github.branch 1086 | ), 1087 | response = UrlFetchApp.fetch(requestUrl, { 1088 | 'method': 'PUT', 1089 | 'headers': { 1090 | 'Accept': 'Accept: application/vnd.github.v3+json', 1091 | 'Authorization': Utilities.formatString('Bearer %s', github.accessToken) 1092 | }, 1093 | 'payload': JSON.stringify({ 1094 | 'message': github.commitMessage, 1095 | 'sha': sha, 1096 | 'content': Utilities.base64Encode(json, Utilities.Charset.UTF_8) 1097 | }) 1098 | }) 1099 | } 1100 | 1101 | /***************************************************************************************************/ 1102 | 1103 | 1104 | /** 1105 | * Go get the sha of the github element we want to replace. 1106 | * 1107 | * @param {inMsg} - message to possibly print to log 1108 | * @returns {String} - sha from the github object 1109 | */ 1110 | function getGitHubSha() { 1111 | 1112 | var requestUrl = Utilities.formatString( 1113 | 'https://api.github.com/repos/%s/%s/contents/%s', 1114 | github.owner, 1115 | github.repo, 1116 | github.path, 1117 | github.branch 1118 | ), 1119 | response = UrlFetchApp.fetch(requestUrl, { 1120 | 'method': 'GET', 1121 | 'headers': { 1122 | 'Accept': 'Accept: application/vnd.github+json', 1123 | 'Authorization': Utilities.formatString('Bearer %s', github.accessToken) 1124 | } 1125 | }); 1126 | 1127 | return JSON.parse(response.getContentText()).sha; 1128 | } 1129 | 1130 | /***************************************************************************************************/ 1131 | 1132 | 1133 | /** 1134 | * Standard recursive divide/conquor QuickSort to sort on DATE field 1135 | * of Item objects. 1136 | * 1137 | * @param {arr} - Array of Item objects to divide into partitions and compare 1138 | * @param {low} - Index of arr's lower bound 1139 | * @param {high} - Index of arr's upper bound 1140 | * @returns {arr} - Array to return 1141 | */ 1142 | function quickSortOnObjectDate(arr, low, high) 1143 | { 1144 | if (low < high) { 1145 | 1146 | var p = partDate(arr, low, high); 1147 | arr = quickSortOnObjectDate(arr, low, p-1); 1148 | arr = quickSortOnObjectDate(arr, p+1, high); 1149 | } 1150 | return arr; 1151 | } 1152 | 1153 | /***************************************************************************************************/ 1154 | 1155 | 1156 | /** 1157 | * Standard partition, pivot, and compare for QuickSort using DATE field of Item object. 1158 | * 1159 | * @param {arr} - Array of Item objects 1160 | * @returns {arr} - Partitioned Array 1161 | */ 1162 | function partDate(arr, low, high) 1163 | { 1164 | var p = arr[high]; 1165 | var i = (low - 1); 1166 | var temp; 1167 | 1168 | for (var j = low; j <= high - 1; j++) { 1169 | 1170 | if (arr[j].date > p.date) { 1171 | 1172 | i++; 1173 | 1174 | temp = arr[i]; 1175 | arr[i] = arr[j]; 1176 | arr[j] = temp; 1177 | } 1178 | } 1179 | 1180 | temp = arr[i+1]; 1181 | arr[i+1] = arr[high]; 1182 | arr[high] = temp; 1183 | 1184 | return (i + 1); 1185 | } 1186 | 1187 | /***************************************************************************************************/ 1188 | 1189 | 1190 | /** 1191 | * Standard recursive divide/conquor QuickSort to sort on ID field 1192 | * of Item objects. 1193 | * 1194 | * @param {arr} - Array of Item objects to divide into partitions and compare 1195 | * @param {low} - Index of arr's lower bound 1196 | * @param {high} - Index of arr's upper bound 1197 | * @returns {arr} - Array to return 1198 | */ 1199 | function quickSortOnObjectId(arr, low, high) { 1200 | if (low < high) { 1201 | 1202 | var p = partId(arr, low, high); 1203 | arr = quickSortOnObjectId(arr, low, p-1); 1204 | arr = quickSortOnObjectId(arr, p+1, high); 1205 | } 1206 | return arr; 1207 | } 1208 | 1209 | /** 1210 | * Standard partition, pivot, and compare for QuickSort using id field of Item object. 1211 | * 1212 | * @param {arr} - Array of Item objects 1213 | * @returns {arr} - Partitioned Array 1214 | */ 1215 | function partId(arr, low, high) { 1216 | var p = arr[high]; 1217 | var i = (low - 1); 1218 | var temp; 1219 | 1220 | for (var j = low; j <= high - 1; j++) { 1221 | 1222 | if (arr[j].id.toLowerCase() < p.id.toLowerCase()) { 1223 | 1224 | i++; 1225 | 1226 | temp = arr[i]; 1227 | arr[i] = arr[j]; 1228 | arr[j] = temp; 1229 | } 1230 | } 1231 | 1232 | temp = arr[i+1]; 1233 | arr[i+1] = arr[high]; 1234 | arr[high] = temp; 1235 | 1236 | return (i + 1); 1237 | } 1238 | 1239 | /***************************************************************************************************/ 1240 | 1241 | /** 1242 | * Standard recursive divide/conquor QuickSort to sort on CSP field 1243 | * of Item objects. 1244 | * 1245 | * @param {arr} - Array of Item objects to divide into partitions and compare 1246 | * @param {low} - Index of arr's lower bound 1247 | * @param {high} - Index of arr's upper bound 1248 | * @returns {arr} - Array to return 1249 | */ 1250 | function quickSortOnObjectCSP(arr, low, high) { 1251 | if (low < high) { 1252 | 1253 | var p = partCSP(arr, low, high); 1254 | arr = quickSortOnObjectCSP(arr, low, p-1); 1255 | arr = quickSortOnObjectCSP(arr, p+1, high); 1256 | } 1257 | return arr; 1258 | } 1259 | 1260 | /***************************************************************************************************/ 1261 | 1262 | 1263 | /** 1264 | * Standard partition, pivot, and compare for QuickSort using CSP field of Item object. 1265 | * 1266 | * @param {arr} - Array of Item objects 1267 | * @returns {arr} - Partitioned Array 1268 | */ 1269 | function partCSP(arr, low, high) { 1270 | var p = arr[high]; 1271 | var i = (low - 1); 1272 | var temp; 1273 | 1274 | for (var j = low; j <= high - 1; j++) { 1275 | 1276 | if (arr[j].csp.toLowerCase() < p.csp.toLowerCase()) { 1277 | 1278 | i++; 1279 | 1280 | temp = arr[i]; 1281 | arr[i] = arr[j]; 1282 | arr[j] = temp; 1283 | } 1284 | } 1285 | 1286 | temp = arr[i+1]; 1287 | arr[i+1] = arr[high]; 1288 | arr[high] = temp; 1289 | 1290 | return (i + 1); 1291 | } 1292 | 1293 | /***************************************************************************************************/ 1294 | 1295 | 1296 | /** 1297 | * Returns "Yes" or "No" from 'Y' or not 'Y' input. For example, the "Small Business" field contains Y/N 1298 | * but the front end website requires Yes/No. 1299 | * 1300 | * @param {inChar} - Character to interrogate. 1301 | * @returns {literal} - "Yes" or "No" depending on 1302 | */ 1303 | 1304 | function getYesNo(inChar) { 1305 | 1306 | if(inChar == "Y") { 1307 | 1308 | return "Yes"; 1309 | } 1310 | 1311 | return "No"; 1312 | } 1313 | 1314 | /***************************************************************************************************/ 1315 | 1316 | 1317 | /** 1318 | * The front end list filtering for products/agencies/assessors requires filtering on number 1319 | * ranges of authorizations and reuses. 1320 | * 1321 | * @param {label} - Label to apply to the middle of the filter name. 1322 | * @returns {s} - Filter name created. 1323 | */ 1324 | function getFilterClassBucket(label, num) { 1325 | 1326 | var s = " filter-" + label + "-"; 1327 | 1328 | if(num <= 5) { 1329 | 1330 | s += "1"; 1331 | } else if (num <= 10) { 1332 | 1333 | s += "2"; 1334 | } else if (num <= 20) { 1335 | 1336 | s += "3"; 1337 | } else { 1338 | 1339 | s += "4"; 1340 | } 1341 | 1342 | return s; 1343 | } 1344 | 1345 | /***************************************************************************************************/ 1346 | 1347 | 1348 | /** 1349 | * Concat strings for sorting. In order to sort nicely, the "parent" and "sub" are placed into 80 byte buffers. 1350 | * 1351 | * @param {s1} - String to be concatenated and returned 1352 | * @param {s2} - String to be concatenated and returned 1353 | * @returns - String of two 80 byte strings 1354 | */ 1355 | function concatParentSub(s1, s2) { 1356 | 1357 | s1 = s1 + STRING_80_BYTES; 1358 | s1 = s1.slice(0,80); 1359 | 1360 | s2 = s2 + STRING_80_BYTES; 1361 | s2 = s2.slice(0,80); 1362 | 1363 | return (s1 + s2).replace(REGEX_NON_WORDS," ").trim(); 1364 | 1365 | } 1366 | 1367 | /***************************************************************************************************/ 1368 | 1369 | 1370 | /** 1371 | * Get unique list of impact level and status for filtering agencies/assessors. 1372 | * 1373 | * @param {arr} - Array of Item objects containing product impact level and status. 1374 | * @returns {literal} - String of unique class names. 1375 | */ 1376 | function getFilterClassImpactAndOffering(arr) { 1377 | 1378 | var classArr = []; 1379 | 1380 | if (arr.length == 0) { 1381 | return ""; 1382 | } 1383 | 1384 | for (var i = 0; i < arr.length; i++) { 1385 | classArr.push(" filter-impact-level-" + arr[i].impact_level.replace(REGEX_NON_WORDS,"-")); 1386 | classArr.push(" filter-status-" + arr[i].status.replace(REGEX_NON_WORDS,"-")); 1387 | } 1388 | 1389 | return Array.from(new Set(classArr)).sort().join(''); 1390 | 1391 | } 1392 | 1393 | /***************************************************************************************************/ 1394 | 1395 | 1396 | /** 1397 | * Super secret number to hide in front of the impact_level so that it doesn't sort by alpha 1398 | * 1399 | * @param {inLevel} - Impact Level value 1400 | * @returns {literal} - 1 thorugh 4 for sorting 1401 | */ 1402 | function getImpactLevelNumber(inLevel) { 1403 | 1404 | if(inLevel == "LI-SaaS") { 1405 | return "1"; 1406 | } 1407 | if(inLevel == "Low") { 1408 | return "2"; 1409 | } 1410 | if(inLevel == "Moderate") { 1411 | return "3"; 1412 | } 1413 | return "4"; 1414 | } 1415 | 1416 | /***************************************************************************************************/ 1417 | --------------------------------------------------------------------------------