Zenn
🦆

【GAS】macOSのバージョンとCVE情報を紐づけてみる

2025/03/20に公開

なぜ作成したのか

  • せっかく脆弱性情報の見方をまとめたので、何かしら役に立たないか試行したい気分

技術スタック

やりたいこと

  • SoFaのJSONファイルからmacOSのバージョン情報、関連CVE番号の情報を取得する
  • 登場するCVE番号についてNVDのAPIで詳細情報を取得する
  • AppsheetでOSバージョンとCVE情報を紐づけて見えるようにする

GAS

  • JSONで記録されているすべてのバージョンを取得する場合は「Latest」ではなく「SecurityReleases」で取得し、バージョン数文繰り返し処理する
  • 今回はめんどくさかったので取り合えず最新のバージョン情報だけ取得する「Latest」の情報でテストする(件数多いとNVDのAPI部分の403エラーが頻発する)
// スクリプトプロパティからスプレッドシートIDを取得
const SPREADSHEET_ID = PropertiesService.getScriptProperties().getProperty('SPREADSHEET_ID');
const JSON_URL = 'https://sofafeed.macadmins.io/v1/macos_data_feed.json';
const CVE_API_BASE = 'https://services.nvd.nist.gov/rest/json/cves/2.0?cveId=';

function main() {
  const data = fetchJsonData();
  const spreadsheet = SpreadsheetApp.openById(SPREADSHEET_ID);

  updateOsVersionSheet(spreadsheet, data);
  updateOsVersionCVEsSheet(spreadsheet, data);
  updateOsVersionExploitedCVEsSheet(spreadsheet, data);
  updateWorkCVEsSheet(spreadsheet, data);
  updateCVEsSheet(spreadsheet);
}

function fetchJsonData() {
  const response = UrlFetchApp.fetch(JSON_URL);
  return JSON.parse(response.getContentText());
}

function updateOsVersionSheet(spreadsheet, data) {
  const sheet = getOrCreateSheet(spreadsheet, 'OsVersion');
  sheet.clear();
  sheet.appendRow(['OSVersion', 'ProductVersion', 'ReleaseDate', 'ExpirationDate', 'SecurityInfo']);

  data.OSVersions.forEach(entry => {
    sheet.appendRow([
      entry.OSVersion,
      entry.Latest.ProductVersion,
      entry.Latest.ReleaseDate,
      entry.Latest.ExpirationDate,
      entry.Latest.SecurityInfo
    ]);
  });
}

function updateOsVersionCVEsSheet(spreadsheet, data) {
  const sheet = getOrCreateSheet(spreadsheet, 'OsVersion-CVEs');
  sheet.clear();
  sheet.appendRow(['id', 'ProductVersion', 'CVE', 'validation']);

  let id = 1;
  data.OSVersions.forEach(entry => {
    if (entry.Latest.CVEs) {
      Object.entries(entry.Latest.CVEs).forEach(([cve, isValid]) => {
        if (cve.startsWith('CVE-')) {
          sheet.appendRow([id++, entry.Latest.ProductVersion, cve, isValid]);
        }
      });
    }
  });
}

function updateOsVersionExploitedCVEsSheet(spreadsheet, data) {
  const sheet = getOrCreateSheet(spreadsheet, 'OsVersion-ActivelyExploitedCVEs');
  sheet.clear();
  sheet.appendRow(['id', 'ProductVersion', 'ActivelyExploitedCVE']);

  let id = 1;
  data.OSVersions.forEach(entry => {
    if (entry.Latest.ActivelyExploitedCVEs) {
      entry.Latest.ActivelyExploitedCVEs.forEach(cve => {
        if (cve.startsWith('CVE-')) {
          sheet.appendRow([id++, entry.Latest.ProductVersion, cve]);
        }
      });
    }
  });
}

function updateWorkCVEsSheet(spreadsheet, data) {
  const sheet = getOrCreateSheet(spreadsheet, 'work-CVEs');
  sheet.clear();
  sheet.appendRow(['CVE']);

  const uniqueCVEs = new Set();
  data.OSVersions.forEach(entry => {
    if (entry.Latest.CVEs) {
      Object.keys(entry.Latest.CVEs).forEach(cve => {
        if (cve.startsWith('CVE-')) {
          uniqueCVEs.add(cve);
        }
      });
    }
  });

  Array.from(uniqueCVEs).forEach(cve => sheet.appendRow([cve]));
}

function updateCVEsSheet(spreadsheet) {
  const workSheet = spreadsheet.getSheetByName('work-CVEs');
  const cveSheet = getOrCreateSheet(spreadsheet, 'CVEs');

  const existingCVEs = new Set(cveSheet.getRange('A2:A').getValues().flat());
  const newCVEs = workSheet.getRange('A2:A').getValues().flat().filter(cve => !existingCVEs.has(cve));

  newCVEs.forEach(cve => {
    const response = UrlFetchApp.fetch(`${CVE_API_BASE}${cve}`);
    const cveData = JSON.parse(response.getContentText()).vulnerabilities[0].cve;

    var tcve =  cve;
    var tpublished =  cveData.published;
    var tlastModified = cveData.lastModified;
    var tvulnStatus =  cveData.vulnStatus;
    if (cveData.descriptions){ 
      var tdescription =  cveData.descriptions[0].value;
    } else { 
      var tdescription =  '';
    } 
    if (cveData.metrics.cvssMetricV31){
      var tvectorString = cveData.metrics.cvssMetricV31[0].cvssData.vectorString;
      var tbaseScore = cveData.metrics.cvssMetricV31[0].cvssData.baseScore;
      var tbaseSeverity = cveData.metrics.cvssMetricV31[0].cvssData.baseSeverity;
      var tattackVector = cveData.metrics.cvssMetricV31[0].cvssData.attackVector;
      var tattackComplexity = cveData.metrics.cvssMetricV31[0].cvssData.attackComplexity;
      var tprivilegesRequired = cveData.metrics.cvssMetricV31[0].cvssData.privilegesRequired;
      var tuserInteraction = cveData.metrics.cvssMetricV31[0].cvssData.userInteraction;
      var tconfidentialityImpact = cveData.metrics.cvssMetricV31[0].cvssData.confidentialityImpact;
      var tintegrityImpact = cveData.metrics.cvssMetricV31[0].cvssData.integrityImpact;
      var tavailabilityImpact = cveData.metrics.cvssMetricV31[0].cvssData.availabilityImpact;
      var texploitabilityScore = cveData.metrics.cvssMetricV31[0].exploitabilityScore;
      var timpactScore = cveData.metrics.cvssMetricV31[0].impactScore;
    } else {
      var tvectorString = '';
      var tbaseScore = '';
      var tbaseSeverity = '';
      var tattackVector = '';
      var tattackComplexity = '';
      var tprivilegesRequired = '';
      var tuserInteraction = '';
      var tconfidentialityImpact = '';
      var tintegrityImpact = '';
      var tavailabilityImpact = '';
      var texploitabilityScore = '';
      var timpactScore = '';
    }
    if (cveData.metrics.cvssMetricV2){
      var t2VectorString = cveData.metrics.cvssMetricV2[0].cvssData.VectorString;
      var t2baseScore = cveData.metrics.cvssMetricV2[0].cvssData.baseScore;
      var t2accessVector = cveData.metrics.cvssMetricV2[0].cvssData.accessVector;
      var t2accessComplexity = cveData.metrics.cvssMetricV2[0].cvssData.accessComplexity;
      var t2authentication = cveData.metrics.cvssMetricV2[0].cvssData.authentication;
      var t2confidentialityImpact = cveData.metrics.cvssMetricV2[0].cvssData.confidentialityImpact;
      var t2integrityImpact = cveData.metrics.cvssMetricV2[0].cvssData.integrityImpact;
      var t2availabilityImpact = cveData.metrics.cvssMetricV2[0].cvssData.availabilityImpact;
      var t2baseSeverity = cveData.metrics.cvssMetricV2[0].baseSeverity;
      var t2exploitabilityScore = cveData.metrics.cvssMetricV2[0].exploitabilityScore;
      var t2impactScore = cveData.metrics.cvssMetricV2[0].impactScore;
      var t2acInsurfInfo = cveData.metrics.cvssMetricV2[0].acInsurfInfo;
      var t2obtainAllPrivilege = cveData.metrics.cvssMetricV2[0].obtainAllPrivilege;
      var t2obtainUserPrivilege = cveData.metrics.cvssMetricV2[0].obtainUserPrivilege;
      var t2obtainOtherPrivilege = cveData.metrics.cvssMetricV2[0].obtainOtherPrivilege;
      var t2userInteractionRequired = cveData.metrics.cvssMetricV2[0].userInteractionRequired;
    } else {
      var t2VectorString = '';
      var t2baseScore = '';
      var t2accessVector = '';
      var t2accessComplexity = '';
      var t2authentication = '';
      var t2confidentialityImpact = '';
      var t2integrityImpact = '';
      var t2availabilityImpact = '';
      var t2baseSeverity = '';
      var t2exploitabilityScore = '';
      var t2impactScore = '';
      var t2acInsurfInfo = '';
      var t2obtainAllPrivilege = '';
      var t2obtainUserPrivilege = '';
      var t2obtainOtherPrivilege = '';
      var t2userInteractionRequired = '';
    }

    const row = [
      tcve,
      tpublished,
      tlastModified,
      tvulnStatus,
      tdescription,
      tvectorString,
      tbaseScore,
      tbaseSeverity,
      tattackVector,
      tattackComplexity,
      tprivilegesRequired,
      tuserInteraction,
      tconfidentialityImpact,
      tintegrityImpact,
      tavailabilityImpact,
      texploitabilityScore,
      timpactScore,
      t2VectorString,
      t2baseScore,
      t2accessVector,
      t2accessComplexity,
      t2authentication,
      t2confidentialityImpact,
      t2integrityImpact,
      t2availabilityImpact,
      t2baseSeverity,
      t2exploitabilityScore,
      t2impactScore,
      t2acInsurfInfo,
      t2obtainAllPrivilege,
      t2obtainUserPrivilege,
      t2obtainOtherPrivilege,
      t2userInteractionRequired
    ];

    cveSheet.appendRow(row);
  });
}

function getOrCreateSheet(spreadsheet, sheetName) {
  const sheet = spreadsheet.getSheetByName(sheetName);
  return sheet || spreadsheet.insertSheet(sheetName);
}

SpreadSheet

  • Sheet: CVEs
  • Sheet: OsVersion-ActivelyExploitedCVEs
  • Sheet: OsVersion-CVEs
  • Sheet: work-CVEs
  • Sheet: OsVersion

Appsheet

所感

  • ChatGPTくんに基礎実装してもらったけど、JSONのインプットが甘かったせいかエラーが頻発してお話にならなかったので、値の取得部分はほとんど作り直した。うまく指示できるひとになりたい
  • このあたりを毎日回しておけば、今回のアップデートでどれくらいヤバい脆弱性対応されてるのかなーというのが件数として把握できそう
  • 同様にWindows側もMSRCのRSSFeed情報を基にCVE情報との紐づけできるような気がする。明日暇だったら実装してみようかな
GitHubで編集を提案

Discussion

ログインするとコメントできます