[
  {
    "path": ".github/workflows/cvss-bt.yml",
    "content": "name: Run Enrichment Process\n\non:\n  workflow_run:\n    workflows: [\"Check for new EPSS data\"]\n    types:\n      - completed\n  workflow_dispatch:\n\npermissions:\n  contents: write\n\njobs:\n  publish:\n    runs-on: ubuntu-latest\n    if: ${{ github.event.workflow_run.conclusion == 'success' }}\n    steps:\n    - name: Checkout repository\n      uses: actions/checkout@v2\n      with:\n        ref: ${{ github.event.workflow_run.head_branch }}\n\n    - name: Set up Python\n      uses: actions/setup-python@v2\n      with:\n        python-version: '3.11'\n\n    - name: Install dependencies\n      run: |\n        pip install -r code/requirements.txt\n\n    - name: Grab Needed Data\n      env:\n        VULNCHECK_API_KEY: ${{ secrets.VULNCHECK_API_KEY }}\n      run: |\n        rm -f *.zip\n        RESPONSE=$(curl --request GET \\\n          --url https://api.vulncheck.com/v3/backup/nist-nvd \\\n          --header 'Accept: application/json' \\\n          --header \"Authorization: Bearer $VULNCHECK_API_KEY\")\n        url=$(echo \"$RESPONSE\" | jq -r '.data[0].url')\n        curl -L -o nvd.zip $url\n        unzip -o \"*.zip\"\n        rm -f *.zip\n\n    - name: Run Enrichment Process\n      timeout-minutes: 60\n      env:\n        VULNCHECK_API_KEY: ${{ secrets.VULNCHECK_API_KEY }}\n      run: |\n        python -u code/process_nvd.py\n\n    - name: setup git config\n      run: |\n        git config user.name \"GitHub Actions Bot\"\n        git config user.email \"<>\"\n\n    - name: Generate tag\n      id: generate_tag\n      run: |\n        echo \"::set-output name=tag::v$(date +%Y.%m.%d)\"\n\n    - name: Commit and push if there are changes\n      run: |\n        git add cvss-bt.csv code/last_run.txt\n        git commit -m \"Updated CVSS-BT data $(date +%Y-%m-%d)\" -a || exit 0\n        git push origin HEAD:main\n        tag=\"${{ steps.generate_tag.outputs.tag }}\"\n        git tag -a \"$tag\" -m \"Updated CVSS-BT data $(date +%Y-%m-%d)\"\n        git push origin \"$tag\"\n\n    - name: Create Release\n      id: create_release\n      uses: actions/create-release@v1\n      env:\n        GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n      with:\n        tag_name: ${{ steps.generate_tag.outputs.tag }}\n        release_name: \"Release ${{ steps.generate_tag.outputs.tag }}\"\n        draft: false\n        prerelease: false\n\n    - name: Upload Release Asset\n      id: upload-release-asset \n      uses: actions/upload-release-asset@v1\n      env:\n        GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n      with:\n        upload_url: ${{ steps.create_release.outputs.upload_url }}\n        asset_path: ./cvss-bt.csv\n        asset_name: cvss-bt.csv\n        asset_content_type: text/plain\n"
  },
  {
    "path": ".github/workflows/epss.yml",
    "content": "name: Check for new EPSS data\n\non:\n    schedule:\n      - cron: \"0 11-15 * * *\" # Every day from 11am to 4pm UTC, 7am to 11am EST\n    push:\n      branches:\n        - dev\n    workflow_dispatch:\n\npermissions:\n    contents: write\n\njobs:\n    check-epss:\n        runs-on: ubuntu-latest\n        steps:\n            - name: Checkout code dir\n              uses: actions/checkout@v2\n\n            - name: Read and Compare Date in Last Run File\n              run: |\n                LAST_RUN_DATE=$(cut -d'T' -f1 code/last_run.txt)\n                CURRENT_DATE=$(date -u +\"%Y-%m-%d\")\n                echo \"Last Run Date: $LAST_RUN_DATE\"\n                echo \"Current Date: $CURRENT_DATE\"\n                if [[ \"$LAST_RUN_DATE\" == \"$CURRENT_DATE\" ]]; then\n                  echo \"The enrichment has already run today. Exiting.\"\n                  exit 1\n                else\n                  echo \"The enrichment has not run today. Proceeding with further steps.\"\n                fi\n\n            - name: Check for new EPSS scores\n              run: |\n                today=$(TZ=America/New_York date +%Y-%m-%d)\n                url=\"https://epss.empiricalsecurity.com/epss_scores-${today}.csv.gz\"\n                echo $url\n                response=$(curl -s -o /dev/null -w \"%{http_code}\" \"$url\")\n                echo $response\n                if [ \"$response\" -eq 200 ]; then\n                  echo \"EPSS scores available for today\"\n                else\n                  echo \"EPSS scores not available yet for today\"\n                  exit 1\n                fi\n"
  },
  {
    "path": ".gitignore",
    "content": ".venv\n.DS_Store\n.local\n*.json\n*.zip\ncode/__pycache__/\n"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2023 Stephen Shaffer\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "# cvss-bt\nThis project enriches the NVD CVSS scores to include Temporal/Threat Metrics, and publishes a CSV file daily with the CVSS-BT scores and information sources.\n\n## Overview\n\nThe Common Vulnerability Scoring System (CVSS) is an industry standard for assessing the severity of computer system security vulnerabilities. CVSS attempts to establish a measure of how severe a vulnerability is based on its attributes.\n\nThe National Vulnerability Database includes CVSS Base scores in its catalog, but base scores are not enough to effectively prioritizie or contextualize vulnerabilities. In this repository I continuously enrich the CVSS score by using the Exploit Code Maturity/Exploitability (E) Temporal Metric.\n\n### Temporal Metric - Exploit Code Maturity/Exploitability (E)\n\nSources:\n- https://www.first.org/cvss/v4-0/cvss-v40-specification.pdf\n- https://www.first.org/cvss/v3.1/specification-document\n- https://www.first.org/cvss/v3.0/specification-document\n- https://www.first.org/cvss/v2/guide\n\n| Value | Description | CVE Present In |\n|---------------------------|-------------|-------------|\n| Attacked (A) (v4.0) | Based on available threat intelligence either of the following must apply: Attacks targeting this vulnerability (attempted or successful) have been reported. Solutions to simplify attempts to exploit the vulnerability are publicly or privately available (such as exploit toolkits) | [CISA KEV](https://www.cisa.gov/known-exploited-vulnerabilities-catalog), [VulnCheck KEV](https://vulncheck.com/kev), [EPSS](https://www.first.org/epss/) > Threshold, [Metasploit](https://www.metasploit.com/) |\n| High (H) (v3.1/3.0/2.0)| Functional autonomous code exists, or no exploit is required (manual trigger) and details are widely available. Exploit code works in every situation, or is actively being delivered via an autonomous agent (such as a worm or virus). Network-connected systems are likely to encounter scanning or exploitation attempts. Exploit development has reached the level of reliable, widely available, easy-to-use automated tools. | [CISA KEV](https://www.cisa.gov/known-exploited-vulnerabilities-catalog), [VulnCheck KEV](https://vulncheck.com/kev), [EPSS](https://www.first.org/epss/) > Threshold, [Metasploit](https://www.metasploit.com/) |\n| Functional (F) (v3.1/3.0/2.0) | Functional exploit code is available. The code works in most situations where the vulnerability exists. | [Nuclei](https://github.com/projectdiscovery/nuclei) |\n| Proof-of-Concept (P) (v4.0/3.1/3.0/2.0) | Proof-of-concept exploit code is available. The code might not work in all situations. | [ExploitDB](https://www.exploit-db.com/), [PoC-in-GitHub](https://github.com/nomi-sec/PoC-in-GitHub) |\n| Unproven (U) (v4.0/3.1/3.0/2.0) | No exploit code is available, or an exploit is theoretical. | CVE not present in any threat intelligence source above. |\n| Not Defined (X) (v4.0/3.1/3.0/2.0) | Assigning this value to the metric will not influence the score. It means the user does not have enough information to assign a score. | We drop this value since we have information to assign a score. |\n\n\n## Features\nThis repository continuously enriches and publishes CVSS Temporal Scores based on the following threat intelligence:\n\n- CISA KEV\n- VulnCheck KEV\n- EPSS\n- Metasploit\n- Nuclei\n- ExploitDB\n- PoC-in-GitHub\n\n### Steps\n- Fetches EPSS scores every morning\n- Fetches CVSS scores from NVD if there are new EPSS scores.\n- Calculates the Exploit Code Maturity/Exploitability (E) Metric when new data is found.\n- Provides a resulting CVSS-BT score for each CVE\n\n## Caveats\n- In the event that the NVD calculated score is using a lesser version than a secondary source, I use the higher CVSS version.\n- The EPSS threshold for returning an `E:H` or `E:A` value is .36, or 36%. This is based on the F1 score of the model and the 37% threshold where most CVEs have weaponized exploit code.\n- I do not recommend using this percentage as a general threshold to prioritize on.\n\n## CVSS Visual Mapping\nThis data visualization provides a breakdown of how the CVSS-B, CVSS-BT and CVSS enriched temporal metrics map to the defined OSINT sources as of November 25th, 2023\n![CVSS-BT Mapping](CVSS-BT-Enrichment.png)\n\n## Acknowledgements\n\nThis product uses VulnCheck KEV.\n\nThis product uses EPSS scores but is not endoresed or certified by the EPSS SIG.\n\n# Support this project\nIf you'd like to financially support this project, feel free to donate below.\n\n[![\"Buy Me A Coffee\"](https://www.buymeacoffee.com/assets/img/custom_images/orange_img.png)](https://www.buymeacoffee.com/stephenshaffer)\n\n"
  },
  {
    "path": "code/enrich_nvd.py",
    "content": "import requests\nimport os\nimport re\nimport pandas as pd\nimport cvss\n\n\nEPSS_CSV = 'data/epss/epss_scores.csv'\nMETASPLOIT_JSON = 'https://raw.githubusercontent.com/rapid7/metasploit-framework/master/db/modules_metadata_base.json'\nNUCLEI_JSON = 'https://raw.githubusercontent.com/projectdiscovery/nuclei-templates/main/cves.json'\nEXPLOITDB_CSV = 'https://gitlab.com/exploit-database/exploitdb/-/raw/main/files_exploits.csv'\nKEV_JSON = \"https://www.cisa.gov/sites/default/files/feeds/known_exploited_vulnerabilities.json\"\nPOC_GITHUB = \"https://raw.githubusercontent.com/nomi-sec/PoC-in-GitHub/master/README.md\"\nVULNCHECK_KEV = 'https://api.vulncheck.com/v3/index/vulncheck-kev'\nVULNCHECK_API_KEY = os.environ.get('VULNCHECK_API_KEY')\n\nEPSS_THRESHOLD = 0.36\n\"\"\"\n36% is the threshold correlated to the F1 score of EPSSv3 model\nAt ~37%, the CVE is very likely to have weaponized exploit code\n\"\"\"\n\ndef enrich(df, epss_df):\n    \"\"\"\n    Enrich CVE data with EPSS, KEV, ExploitDB, Metasploit, and Nuclei data\n    \"\"\"\n\n    #Load KEV Data\n    response = requests.get(KEV_JSON)\n    kev_json_data = response.json()\n    kev_cve_list = []\n    for vuln in kev_json_data.get('vulnerabilities'):\n        kev_cve_list.append(vuln.get('cveID'))\n    kev_df = pd.DataFrame(kev_cve_list, columns=['cve'])\n    kev_df['cisa_kev'] = True\n    \n    #Load VulnCheck KEV\n    vulncheck_kev = get_vulncheck_data()\n    vulncheck_kev_df = pd.DataFrame(vulncheck_kev, columns=['cve'])\n    vulncheck_kev_df['cve'] = vulncheck_kev_df['cve'].apply(lambda x: ', '.join(map(str, x)))\n    vulncheck_kev_df['vulncheck_kev'] = True\n\n    #Load ExploitDB\n    exploitdb_df = pd.read_csv(EXPLOITDB_CSV, usecols=['codes']).rename(columns={\"codes\": \"cve\"})\n    exploitdb_df.drop_duplicates(inplace=True)\n    exploitdb_df = exploitdb_df['cve'].str.extract(r\"(CVE-\\d{4}-\\d{4,7})\", expand=False).dropna().values\n    exploitdb_df = pd.DataFrame(exploitdb_df, columns = ['cve'])\n    exploitdb_df['exploitdb'] = True\n\n    #Load Metasploit\n    response = requests.get(METASPLOIT_JSON)\n    ms_json_data = response.json()\n    ms_cve_list = []\n    for item in ms_json_data:\n        if 'references' in ms_json_data[item]:\n            cve_references = [ref for ref in ms_json_data[item]['references'] if ref.startswith('CVE-')]\n            ms_cve_list.extend(cve_references)\n    metasploit_df = pd.DataFrame(ms_cve_list, columns=['cve'])\n    metasploit_df['metasploit'] = True\n\n    #Load Nuclei\n    nuclei_df = pd.read_json(NUCLEI_JSON, lines=True)\n    nuclei_df.rename(columns={\"ID\": \"cve\"}, inplace=True)\n    nuclei_df = nuclei_df.drop(columns=['Info', 'file_path'])\n    nuclei_df['nuclei'] = True\n\n    #Load Poc-in-GitHub\n    poc_githib_df = pd.DataFrame(extract_cves_from_github(POC_GITHUB), columns=['cve'])\n    poc_githib_df['poc_github'] = True\n\n    print('Mapping EPSS Data')\n    df = pd.merge(df, epss_df, on='cve', how='left')\n\n    print('Mapping KEV Data')\n    df = pd.merge(df, kev_df, on='cve', how='left')\n    \n    print('Mapping VulnCheck KEV Data')\n    df = pd.merge(df, vulncheck_kev_df, on='cve', how='left')\n\n    print('Mapping ExploitDB Data')\n    df = pd.merge(df, exploitdb_df, on='cve', how='left')\n\n    print('Mapping Metasploit Data')\n    df = pd.merge(df, metasploit_df, on='cve', how='left')\n\n    print('Mapping Nuclei Data')\n    df = pd.merge(df, nuclei_df, on='cve', how='left')\n\n    print('Mapping Poc-in-GitHub Data')\n    df = pd.merge(df, poc_githib_df, on='cve', how='left')\n\n    df = df.drop_duplicates(subset='cve')\n    # Fill NaN values appropriately for each column type\n    bool_columns = ['cisa_kev', 'vulncheck_kev', 'exploitdb', 'metasploit', 'nuclei', 'poc_github']\n    df[bool_columns] = df[bool_columns].fillna(False)\n    df['epss'] = df['epss'].fillna(0.0)\n    return df\n\n\ndef extract_cves_from_github(url):\n    response = requests.get(url)\n    if response.status_code == 200:\n        content = response.text\n    else:\n        content = \"\"\n        print(\"Failed to fetch README file\")\n\n    cve_pattern = r\"CVE-\\d{4}-\\d{4,7}\"\n    cve_matches = re.findall(cve_pattern, content)\n    unique_cves = set(cve_matches)\n    return list(unique_cves)\n\n\ndef get_vulncheck_data():\n    data = []\n    headers = {\n      \"accept\": \"application/json\",\n      \"authorization\": f\"Bearer {VULNCHECK_API_KEY}\"\n    }\n    response = requests.get(VULNCHECK_KEV, headers=headers)\n    response = response.json()\n    current_page = response.get('_meta').get('page')\n    total_pages = response.get('_meta').get('total_pages')\n    data.extend(response.get('data'))\n    while current_page < total_pages:\n        current_page += 1\n        response = requests.get(f\"{VULNCHECK_KEV}?page={current_page}\", headers=headers)\n        response = response.json()\n        data.extend(response.get('data'))\n    return data\n\n\ndef update_temporal_score(df, epss_threshold):\n    \"\"\"\n    Update temporal score and severity based on exploit maturity\n    \"\"\"\n    df['exploit_maturity'] = 'E:U'  # Default value\n\n    condition_ea = (df['cisa_kev']) | (df['epss'] >= epss_threshold) | (df['vulncheck_kev']) | (df['metasploit'])\n    condition_ep4 = (~condition_ea) & ((df['nuclei']) | (df['exploitdb'] | df['poc_github']))\n    # First condition for 'E:H'\n    condition_eh = (df['cisa_kev']) | (df['epss'] >= epss_threshold) | (df['vulncheck_kev'])\n    # Next condition for 'E:F'\n    condition_ef = (~condition_eh) & ((df['nuclei']) | (df['metasploit']))\n    # Last condition for 'E:P'\n    condition_ep = (~condition_eh) & (~condition_ef) & (df['exploitdb'] | df['poc_github'])\n\n    df.loc[condition_eh & (df['cvss_version'].astype(str) != '4.0'), 'exploit_maturity'] = 'E:H'\n    df.loc[condition_ea & (df['cvss_version'].astype(str) == '4.0'), 'exploit_maturity'] = 'E:A' #Updated to Attacked for 4.0\n    df.loc[condition_ef & (df['cvss_version'].astype(str) != '4.0'), 'exploit_maturity'] = 'E:F'\n    df.loc[condition_ep & (df['cvss_version'].astype(str) == '2.0'), 'exploit_maturity'] = 'E:POC'\n    df.loc[condition_ep & (df['cvss_version'].astype(str) != '2.0') & (df['cvss_version'].astype(str) != '4.0'), 'exploit_maturity'] = 'E:P'\n    df.loc[condition_ep4 & (df['cvss_version'].astype(str) == '4.0'), 'exploit_maturity'] = 'E:P'\n\n    # Update vector with exploit maturity\n    #Remove \"E:X\" from base vector if it exists\n    df['cvss-bt_vector'] = df.apply(lambda row: f\"{row['base_vector']}/{row['exploit_maturity']}\" if 'E:X' not in row['base_vector'] and row['base_vector'] != 'N/A' \\\n                                                   else row['base_vector'].replace('/E:X', f\"/{row['exploit_maturity']}\") if row['base_vector'] != 'N/A' \\\n                                                   else row['base_vector'], axis=1)\n\n    # Apply CVSS computation\n    def compute_cvss(row):\n        try:\n            if 'N/A' in str(row['cvss_version']):\n                return 'UNKNOWN', 'UNKNOWN'\n            elif '4' in str(row['cvss_version']):\n                c = cvss.CVSS4(row['cvss-bt_vector'])\n                return c.base_score, str(c.severity).upper()\n            elif '3' in str(row['cvss_version']):\n                c = cvss.CVSS3(row['cvss-bt_vector'])\n                return c.temporal_score, str(c.severities()[1]).upper()\n            elif '2' in str(row['cvss_version']):\n                c = cvss.CVSS2(row['cvss-bt_vector'])\n                return c.temporal_score, str(c.severities()[1]).upper()\n            else:\n                raise ValueError(f\"Unknown CVSS version: {row['cvss_version']}\")\n        except Exception as e:\n            print(f\"Error occurred while computing CVSS for {row['cve']}: {e}\")\n            return 'UNKNOWN', 'UNKNOWN'\n\n    # Extracting CVSS scores and severities\n    print('Computing CVSS-BT scores and severities')\n    df[['cvss-bt_score', 'cvss-bt_severity']] = df.apply(compute_cvss, axis=1, result_type='expand') #Apply function to each row\n\n    return df\n"
  },
  {
    "path": "code/last_run.txt",
    "content": "2026-05-12T13:44:04Z"
  },
  {
    "path": "code/process_nvd.py",
    "content": "from datetime import datetime, date\nfrom pathlib import Path\nimport pandas as pd\nimport enrich_nvd\nimport ijson\n\nEPSS_CSV = f'https://epss.empiricalsecurity.com/epss_scores-{date.today()}.csv.gz'\nTIMESTAMP_FILE = './code/last_run.txt'\n\ndef process_nvd_files():\n    \"\"\"\n    Processes the NVD JSON files incrementally using a streaming parser (ijson).\n    \"\"\"\n    nvd_dict = []\n\n    for file_path in Path('.').glob('*.json'):\n        print(f'Processing {file_path.name}')\n        with file_path.open('r', encoding='utf-8') as file:\n            # Stream over each item in the CVE_Items array\n            for entry in ijson.items(file, 'CVE_Items.item'):\n                if not entry['cve']['description']['description_data'][0]['value'].startswith('**'):\n                    cve = entry['cve']['CVE_data_meta']['ID']\n                    if 'metricV40' in entry['impact']:\n                        cvss_version = '4.0'\n                        base_score = entry['impact']['metricV40']['baseScore']\n                        base_severity = entry['impact']['metricV40']['baseSeverity']\n                        base_vector = entry['impact']['metricV40']['vectorString']\n                    elif 'baseMetricV3' in entry['impact']:\n                        cvss_version = entry['impact']['baseMetricV3']['cvssV3']['version']\n                        base_score = entry['impact']['baseMetricV3']['cvssV3']['baseScore']\n                        base_severity = entry['impact']['baseMetricV3']['cvssV3']['baseSeverity']\n                        base_vector = entry['impact']['baseMetricV3']['cvssV3']['vectorString']\n                    else:\n                        cvss_version = entry['impact'].get('baseMetricV2', {}).get('cvssV2', {}).get('version', 'N/A')\n                        base_score = entry['impact'].get('baseMetricV2', {}).get('cvssV2', {}).get('baseScore', 'N/A')\n                        base_severity = entry['impact'].get('baseMetricV2', {}).get('severity', 'N/A')\n                        base_vector = entry['impact'].get('baseMetricV2', {}).get('cvssV2', {}).get('vectorString', 'N/A')\n                    assigner = entry['cve']['CVE_data_meta']['ASSIGNER']\n                    published_date = entry['publishedDate']\n                    description = entry['cve']['description']['description_data'][0]['value']\n\n                    dict_entry = {\n                        'cve': cve,\n                        'cvss_version': cvss_version,\n                        'base_score': base_score,\n                        'base_severity': base_severity,\n                        'base_vector': base_vector,\n                        'assigner': assigner,\n                        'published_date': published_date,\n                        'description': description\n                    }\n                    nvd_dict.append(dict_entry)\n\n    nvd_df = pd.DataFrame(nvd_dict)\n    print('CVEs with CVSS scores from NVD:', nvd_df['cve'].nunique())\n    return nvd_df\n\n\ndef enrich_df(nvd_df):\n    \"\"\"\n    Enriches the dataframe with exploit maturity and temporal scores.\n    \"\"\"\n\n    print('Enriching data')\n    enriched_df = enrich_nvd.enrich(nvd_df, pd.read_csv(EPSS_CSV, comment='#', compression='gzip'))\n    cvss_bt_df = enrich_nvd.update_temporal_score(enriched_df, enrich_nvd.EPSS_THRESHOLD)\n    \n    columns = [\n        'cve',\n        'cvss-bt_score',\n        'cvss-bt_severity',\n        'cvss-bt_vector',\n        'cvss_version',\n        'base_score',\n        'base_severity',\n        'base_vector',\n        'assigner',\n        'published_date',\n        'epss',\n        'cisa_kev',\n        'vulncheck_kev',\n        'exploitdb',\n        'metasploit',\n        'nuclei',\n        'poc_github'\n    ]\n    cvss_bt_df = cvss_bt_df[columns]\n    cvss_bt_df = cvss_bt_df.sort_values(by=['published_date'])\n    cvss_bt_df = cvss_bt_df.reset_index(drop=True)\n    cvss_bt_df.to_csv('cvss-bt.csv', index=False, mode='w')\n\n\ndef save_last_run_timestamp(filename='last_run.txt'):\n    \"\"\"\n    Save the current timestamp as the last run timestamp in a file.\n\n    Args:\n        filename (str): The name of the file to save the timestamp. Default is 'last_run.txt'.\n    \"\"\"\n    with open(filename, 'w', encoding='utf-8') as f:\n        f.write(datetime.now().strftime('%Y-%m-%dT%H:%M:%SZ'))\n\n\nenrich_df(process_nvd_files())\nsave_last_run_timestamp(TIMESTAMP_FILE)\n"
  },
  {
    "path": "code/requirements.txt",
    "content": "cvss\npandas\nrequests\nijson"
  },
  {
    "path": "test.sh",
    "content": "today=$(TZ=America/New_York date +%Y-%m-%d)\nyear=$(date +%Y)\nurl=\"https://epss.empiricalsecurity.com/epss_scores-${today}.csv.gz\"\necho $url\nresponse=$(curl -s -o /dev/null -w \"%{http_code}\" \"$url\")\necho $response\nif [ \"$response\" -eq 200 ]; then\n  echo \"EPSS scores available for today\"\nelse\n  echo \"EPSS scores not available yet for today\"\n  exit 1\nfi\npip3 install -r code/requirements.txt\nrm -f *.zip\nRESPONSE=$(curl --request GET \\\n          --url https://api.vulncheck.com/v3/backup/nist-nvd \\\n          --header 'Accept: application/json' \\\n          --header \"Authorization: Bearer $VULNCHECK_API_KEY\")\nurl=$(echo \"$RESPONSE\" | jq -r '.data[0].url')\ncurl -L -o nvd.zip $url\nunzip -o \"*.zip\"\nrm -f *.zip\npython3 -u code/process_nvd.py\n"
  }
]