0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Pythonで始めるテストツール製作(5)NVDからCVSSメトリクスを取得しTextileの表形式に整形する

Posted at

1. はじめに

第一世代のQAエンジニア(7)JIS Q 9005で読み解くQAの役割1で述べたようにQAエンジニアの社会的価値の取り組みの一つにセキュリティ対応があります。

ハードウェアやソフトウェアが脆弱性を解消したときに複数のCVE IDを開示することがあります。そこで脆弱性情報をWikiなどへ転記する際、CVSSメトリクスを俯瞰しやすいよう、CVSSメトリクスをTextileの表形式に整形して転記できるようにします。

Textileの代わりにMarkdownに対応する方法は3.4節をご覧ください。

2. CVSSメトリクスの取得

NIST(アメリカ国立標準技術研究所)が提供している脆弱性情報のREST API2を利用させていただきます。注意点として「It is also recommended that users "sleep" their scripts for six seconds between requests.3」とあり連続してアクセスする場合は6秒おきにします。

CVSSメトリクスはCVSS v2とCVSS v3の両方が提供されますが、CVEによってどちらもなかったり、どちらか一方だけ、両方ともあるなどまちまちです。

CVE ID V2(Primary) V2(Secondary) V3(Primary) V3(Secondary)
CVE-2024-36387 - - - -
CVE-2019-1010218 - -
CVE-2024-38475 - - -
CVE-2024-38476 - -

3. テストツール

次のようなテストツールをPythonで作りました。

  • CVE IDの指定方法は1)対話形式で手入力、2)ファイルに記述、の両方に対応
  • ファイルでCVE IDを指定する場合は次の3つのファイルを使用する
    • cvelist.txt(CVE IDを1行1IDで記述する)
    • cvssMetricV20.txt(CVE IDとcvssMetricV2情報を1行1IDで保存する)
    • cvssMetricV31.txt(CVE IDとcvssMetricV31情報を1行1IDで保存する)
  • CVSSメトリクスをTextileの表形式に整形する。
    • CVE IDはNISTのCVE Detailへリンクする
    • metrics情報がない場合:No metricsを記述する
    • cvssMetricV2情報がない場合:No cvssMetricV2を記述する
    • cvssMetricV31情報がない場合:No cvssMetricV31を記述する

3.1 ソースコード

動作確認:Python 3.13.0 / Microsoft Windows 11 Pro 23H2
実行方法:py mytools_cvss_metrics.py
追加ライブラリ:requests Ver. 2.32.3

mytools_cvss_metrics.py
# mytools_cvss_metrics.py by ka's@pbjpkas 2024
# MIT License
#
# references
#   National Vulnerability Database
#     https://nvd.nist.gov/developers/vulnerabilities
#   NVD API Key
#     https://nvd.nist.gov/general/news/API-Key-Announcement
#     **** It is also recommended that users "sleep" their scripts for six seconds between requests. ****
#   NVD公開のREST APIを用いて脆弱性情報を取得する
#     https://qiita.com/riikunn_ryo/items/97e385ed0a78dc28534f
#   PythonでWeb APIを叩いてJSONをパースする
#     https://qiita.com/bow_arrow/items/4dcab3389c892baba1a5
#   Pythonで辞書のキー・値の存在を確認、取得(検索)
#     https://note.nkmk.me/python-dict-in-values-items/
#   Textile Markup Language Documentation
#     https://textile-lang.com/
#
# 動作確認
#   Python 3.13.0 / Microsoft Windows 11 Pro 23H2
#

# py -m pip install requests
import requests
import json
import time

# Textile Table Header
# bS : baseScore
# eS : exploitabilityScore
# iS : impactScore
header_v20 = '|_. CVE ID |_. type |_. vectorString |_. bS |_. baseSeverity |_. eS |_. iS |'
header_v31 = '|_. CVE ID |_. type |_. vectorString |_. bS |_. baseSeverity |_. eS |_. iS |'


def get_cve_detail(cveId):
    url = 'https://services.nvd.nist.gov/rest/json/cves/2.0?cveId=' + cveId
    response = requests.get(url)
    return response


def get_cve_detail_dbg(cveId):
    print(cveId)
    
    response = get_cve_detail(cveId)
    if response.status_code != requests.codes.ok:
        print('RESPONSE is NOT OK', response.status_code)
        return False
    
    jsonData = response.json()
    print(jsonData)
    print('----------')

    if len(jsonData['vulnerabilities'][0]['cve']['metrics']) == 0:
        print('No metrics')
        return False
    else:
        print(jsonData['vulnerabilities'][0]['cve']['descriptions'][0]['value'])
        return True


def notate_cvssMetric_in_Textile_table(cveId):
    cveIdLink = '"' + cveId + '":https://nvd.nist.gov/vuln/detail/' + cveId
    
    response = get_cve_detail(cveId)
    if response.status_code != requests.codes.ok:
        #print('RESPONSE is NOT OK', response.status_code)
        data_v20 = '|' + cveIdLink + '|RESPONSE is NOT OK, ' + str(response.status_code) + '| | | | |'
        data_v31 = '|' + cveIdLink + '|RESPONSE is NOT OK, ' + str(response.status_code) + '| | | | |'
        return data_v20, data_v31
    
    jsonData = response.json()
    if jsonData['vulnerabilities'][0]['cve'].get('metrics') == None:
        #print('No metrics')
        data_v20 = '|' + cveIdLink + '|No metrics| | | | | |'
        data_v31 = '|' + cveIdLink + '|No metrics| | | | | |'
        return data_v20, data_v31
    
    if jsonData['vulnerabilities'][0]['cve']['metrics'].get('cvssMetricV2') == None:
        #print('No cvssMetricV2')
        data_v20 = '|' + cveIdLink + '|No cvssMetricV2| | | | | |'
    else:
        type                    = jsonData['vulnerabilities'][0]['cve']['metrics']['cvssMetricV2'][0]['type']
        vectorString            = jsonData['vulnerabilities'][0]['cve']['metrics']['cvssMetricV2'][0]['cvssData']['vectorString']
        baseScore               = jsonData['vulnerabilities'][0]['cve']['metrics']['cvssMetricV2'][0]['cvssData']['baseScore']
        baseSeverity            = jsonData['vulnerabilities'][0]['cve']['metrics']['cvssMetricV2'][0]['baseSeverity']
        exploitabilityScore     = jsonData['vulnerabilities'][0]['cve']['metrics']['cvssMetricV2'][0]['exploitabilityScore']
        impactScore             = jsonData['vulnerabilities'][0]['cve']['metrics']['cvssMetricV2'][0]['impactScore']
        data_v20 = '|' + cveIdLink +  '|' + type + '|' + vectorString + '|' + str(baseScore) + '|' + baseSeverity + '|' + str(exploitabilityScore) + '|' + str(impactScore) + '|'
    
    if jsonData['vulnerabilities'][0]['cve']['metrics'].get('cvssMetricV31') == None:
        #print('No cvssMetricV31')
        data_v31 = '|' + cveIdLink + '|No cvssMetricV31| | | | | |'
    else:
        type                = jsonData['vulnerabilities'][0]['cve']['metrics']['cvssMetricV31'][0]['type']
        vectorString        = jsonData['vulnerabilities'][0]['cve']['metrics']['cvssMetricV31'][0]['cvssData']['vectorString']
        baseScore           = jsonData['vulnerabilities'][0]['cve']['metrics']['cvssMetricV31'][0]['cvssData']['baseScore']
        baseSeverity        = jsonData['vulnerabilities'][0]['cve']['metrics']['cvssMetricV31'][0]['cvssData']['baseSeverity']
        exploitabilityScore = jsonData['vulnerabilities'][0]['cve']['metrics']['cvssMetricV31'][0]['exploitabilityScore']
        impactScore         = jsonData['vulnerabilities'][0]['cve']['metrics']['cvssMetricV31'][0]['impactScore']
        data_v31 = '|' + cveIdLink +  '|' + type + '|' + vectorString + '|' + str(baseScore) + '|' + baseSeverity + '|' + str(exploitabilityScore) + '|' + str(impactScore) + '|'
    
    return data_v20, data_v31


def notate_cvssMetric_in_Textile_table_from_cvelist_file():
    i_cvelist       = 'cvelist.txt'
    o_cvssMetricV20 = 'cvssMetricV20.txt'
    o_cvssMetricV31 = 'cvssMetricV31.txt'
    
    with open(i_cvelist) as fi:
        line_strip = [line.rstrip() for line in fi.readlines()]
        if len(line_strip) == 0:
            print('NO DATA FOUND')
            return
    
    with open(o_cvssMetricV20, mode='w') as fo_v20:
        with open(o_cvssMetricV31, mode='w') as fo_v31:
            fo_v20.write(header_v20+'\n')
            fo_v31.write(header_v31+'\n')
            for cveid in line_strip:
                data_v20, data_v31 = notate_cvssMetric_in_Textile_table(cveid)
                print(data_v20)
                fo_v20.write(data_v20+'\n')
                print(data_v31)
                fo_v31.write(data_v31+'\n')
                time.sleep(6)


def main():
    while True:
        print('= myTools cvssMetric =')
        print('a: get CVE detail(debug)')
        print('b: notate cvssMetric in Textile Table')
        print('c: notate cvssMetric in Textile Table(from cvelist file)')
        print('x: exit')
        
        s = input('>')
        
        if s == 'a':
            cveId = input('Enter CVE ID(ex:CVE-2019-1010218)>')
            get_cve_detail_dbg(cveId)
            
        if s == 'b':
            cveId = input('Enter CVE ID(ex:CVE-2019-1010218)>')
            data_v20, data_v31 = notate_cvssMetric_in_Textile_table(cveId)
            print(header_v20)
            print(data_v20)
            print(header_v31)
            print(data_v31)
        
        if s == 'c':
            notate_cvssMetric_in_Textile_table_from_cvelist_file()
            
        if s == 'x':
            if __name__ == '__main__':
                print('Bye.')
            return


if __name__ == '__main__':
    main()

3.2 実行例

あらかじめ次のcvelist.txtを作成します。

cvelist.txt
CVE-2024-36387
CVE-2019-1010218
CVE-2024-38475
CVE-2024-38476

Python3で実行し、メニューのcを選びます。

実行例
>py mytools_cvss_metrics.py
= myTools cvssMetric =
a: get CVE detail(debug)
b: notate cvssMetric in Textile Table
c: notate cvssMetric in Textile Table(from cvelist file)
x: exit
>c
|"CVE-2024-36387":https://nvd.nist.gov/vuln/detail/CVE-2024-36387|No cvssMetricV2| | | | | |
|"CVE-2024-36387":https://nvd.nist.gov/vuln/detail/CVE-2024-36387|No cvssMetricV31| | | | | |
|"CVE-2019-1010218":https://nvd.nist.gov/vuln/detail/CVE-2019-1010218|Primary|AV:N/AC:L/Au:N/C:N/I:N/A:P|5.0|MEDIUM|10.0|2.9|
|"CVE-2019-1010218":https://nvd.nist.gov/vuln/detail/CVE-2019-1010218|Primary|CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H|7.5|HIGH|3.9|3.6|
|"CVE-2024-38475":https://nvd.nist.gov/vuln/detail/CVE-2024-38475|No cvssMetricV2| | | | | |
|"CVE-2024-38475":https://nvd.nist.gov/vuln/detail/CVE-2024-38475|Secondary|CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:N|9.1|CRITICAL|3.9|5.2|
|"CVE-2024-38476":https://nvd.nist.gov/vuln/detail/CVE-2024-38476|No cvssMetricV2| | | | | |
|"CVE-2024-38476":https://nvd.nist.gov/vuln/detail/CVE-2024-38476|Primary|CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H|9.8|CRITICAL|3.9|5.9|
= myTools cvssMetric =
a: get CVE detail(debug)
b: notate cvssMetric in Textile Table
c: notate cvssMetric in Textile Table(from cvelist file)
x: exit
>

次のような出力ファイルを得ました。

cvssMetricV20.txtの例
|_. CVE ID |_. type |_. vectorString |_. bS |_. baseSeverity |_. eS |_. iS |
|"CVE-2024-36387":https://nvd.nist.gov/vuln/detail/CVE-2024-36387|No cvssMetricV2| | | | | |
|"CVE-2019-1010218":https://nvd.nist.gov/vuln/detail/CVE-2019-1010218|Primary|AV:N/AC:L/Au:N/C:N/I:N/A:P|5.0|MEDIUM|10.0|2.9|
|"CVE-2024-38475":https://nvd.nist.gov/vuln/detail/CVE-2024-38475|No cvssMetricV2| | | | | |
|"CVE-2024-38476":https://nvd.nist.gov/vuln/detail/CVE-2024-38476|No cvssMetricV2| | | | | |
cvssMetricV31.txtの例
|_. CVE ID |_. type |_. vectorString |_. bS |_. baseSeverity |_. eS |_. iS |
|"CVE-2024-36387":https://nvd.nist.gov/vuln/detail/CVE-2024-36387|No cvssMetricV31| | | | | |
|"CVE-2019-1010218":https://nvd.nist.gov/vuln/detail/CVE-2019-1010218|Primary|CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H|7.5|HIGH|3.9|3.6|
|"CVE-2024-38475":https://nvd.nist.gov/vuln/detail/CVE-2024-38475|Secondary|CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:N|9.1|CRITICAL|3.9|5.2|
|"CVE-2024-38476":https://nvd.nist.gov/vuln/detail/CVE-2024-38476|Primary|CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H|9.8|CRITICAL|3.9|5.9|

3.3 Textile表形式の例

cvssMetricV20.txtおよびcvssMetricV31.txtをTables / Textile Markup Language Documentationへ入力して表示された表を以下に示します。テーブルヘッダのbS、eS、iSはそれぞれ以下の略です。

  • bS : baseScore
  • eS : exploitabilityScore
  • iS : impactScore

cvssMetricV2-TextileTable-sample.png
cvssMetricV31-TextileTable-sample.png

3.4 Markdown対応

Markdownなどテーブルのセルの区切り文字が "|" のマークアップ言語であれば次の3行を改修して対応できます。

  • テーブルのヘッダーの書式:header_v20、header_v31
  • CVE Detailのリンクの書式:cveIdLink

Markdown対応の例を以下に示します。

#header_v20 = '|_. CVE ID |_. type |_. vectorString |_. bS |_. baseSeverity |_. eS |_. iS |'
header_v20 = '| CVE ID | type | vectorString | bS | baseSeverity | eS | iS |\n|----|----|----|----|----|----|----|'

#header_v31 = '|_. CVE ID |_. type |_. vectorString |_. bS |_. baseSeverity |_. eS |_. iS |'
header_v31 = '| CVE ID | type | vectorString | bS | baseSeverity | eS | iS |\n|----|----|----|----|----|----|----|'

#    cveIdLink = '"' + cveId + '":https://nvd.nist.gov/vuln/detail/' + cveId
    cveIdLink = '[' + cveId + '](https://nvd.nist.gov/vuln/detail/' + cveId + ')'
cvssMetricV31.txtの例
| CVE ID | type | vectorString | bS | baseSeverity | eS | iS |
|----|----|----|----|----|----|----|
|[CVE-2024-36387](https://nvd.nist.gov/vuln/detail/CVE-2024-36387)|No cvssMetricV31| | | | | |
|[CVE-2019-1010218](https://nvd.nist.gov/vuln/detail/CVE-2019-1010218)|Primary|CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H|7.5|HIGH|3.9|3.6|
|[CVE-2024-38475](https://nvd.nist.gov/vuln/detail/CVE-2024-38475)|Secondary|CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:N|9.1|CRITICAL|3.9|5.2|
|[CVE-2024-38476](https://nvd.nist.gov/vuln/detail/CVE-2024-38476)|Primary|CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H|9.8|CRITICAL|3.9|5.9|

Markdown表示例

CVE ID type vectorString bS baseSeverity eS iS
CVE-2024-36387 No cvssMetricV31
CVE-2019-1010218 Primary CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H 7.5 HIGH 3.9 3.6
CVE-2024-38475 Secondary CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:N 9.1 CRITICAL 3.9 5.2
CVE-2024-38476 Primary CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H 9.8 CRITICAL 3.9 5.9

4. おわりに

  1. 複数のCVEのCVSSメトリクスを表形式にしたことで俯瞰しやすくなったと思います。
  2. セキュリティ対応はExcel VBAでJVNのREST APIを叩いて返ってきたXMLを加工する記事(ルータの脆弱性情報を備品管理のExcelファイルで自動取得する)を以前書きましたが、今回PythonでNVDのREST APIを叩いて返ってきたJSONを加工してみて、次のような理由で脆弱性情報の収集や加工、分析は思っていたよりもハードルが高くないように思いました4
    • 脆弱性の情報源がすでに用意されている
    • Excel VBAにしてもPythonにしてもググるとREST APIの叩き方やXML、JSONの加工方法を解説している記事が見つかる
    • 今回作成したテストツールはコメントを除くと140行程度とコンパクト
  3. 次のような機能追加は追々試してみたいです。
    • 入力データ(CVE ID)のチェック
      • 今はcvelist.txtが空かどうかのチェックのみ
    • CVSSにPrimaryとSecondaryの両方ある場合にどちらも転記するようにする
      • 今は0番目を決め打ちで転記している

A. 参考資料

B. Pythonで始めるテストツール製作

(1)~(3)をPDF化した「Pythonで始めるテストツール製作 Menu Based CLI編」を技術書典で頒布しています。

  1. 第一世代のQAエンジニアも併せてご覧ください。

  2. Vulnerability APIs

  3. NVD API Key

  4. ダニングクルーガー効果という気がしなくもないですが…

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?