Closed5

Javaの利用ライブラリの脆弱性を調査する

dekimasoondekimasoon

まずは元となるライブラリ一覧。
pom.xmlを見てみると、コード内に配置されたjarを参照しているっぽい。

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
	<dependencies>
		<dependency>
			<groupId>jp.co.xxx.xxx.xxx</groupId>
			<artifactId>aopalliance</artifactId>
			<version>1.0</version>
			<scope>system</scope>
			<systemPath>${basedir}/lib/aopalliance-1.0.jar</systemPath>
		</dependency>
		<dependency>
			<groupId>jp.co.xxx.xxx.xxx</groupId>
			<artifactId>asm</artifactId>
			<version>7.3.1</version>
			<scope>system</scope>
			<systemPath>${basedir}/lib/asm-7.3.1.jar</systemPath>
		</dependency>
...

問題はgroupIdのとこで、本来はライブラリ自体のGroupIDが入ってくるのだけど、すべて開発元の会社名が入ってしまっている。つまりaopallianceという同名のライブラリが複数存在した場合に、どのaopallianceなのかpom.xmlだけでは判断できない可能性がある。

確認したい場合はjarファイルを回答して中を見に行く必要がある。
とても面倒なのでいったんこの問題は放置する。

dekimasoondekimasoon

次にpomファイルをパースしてREST APIを呼び出す際に必要な情報を取得してみる。
JavaScriptしかまともに書けないマンなので、JSでスクリプトを書いてみよう。

import fs from 'fs'
import { XMLParser } from 'fast-xml-parser'

const pom = fs.readFileSync('pom.xml', 'utf-8')
const parser = new XMLParser();
const parsedPom = parser.parse(pom)

const dependencies = parsedPom.project.dependencies.dependency
const formattedDeps = dependencies.map(x => ({
  artifactId: x.artifactId,
  version: String(x.version),
}))

console.log(formattedDeps)

これで以下のような結果が取れるようになった。とりあえずOK。

% node maven_checker.mjs
[
  { artifactId: 'aopalliance', version: '1' },
  { artifactId: 'asm', version: '7.3.1' },
  ...
]
dekimasoondekimasoon

次に、Maven Central RepositoryのAPIの呼び出し方を理解したい。

取得したい項目は以下。

  • 最新バージョン
  • 最新バージョンの更新日
  • 最新バージョンに報告されている脆弱性

ここにまとまっている。
https://central.sonatype.org/search/rest-api-guide/

以下で、今までとれているartifactIdとversionで検索できるっぽい。

> curl "https://search.maven.org/solrsearch/select?q:a=${artifactId}+AND+v:${version}"

戻り値は以下。
複数件返ってくるのもや0件の場合もあるっぽいが、まあいいや。
これで戻りが1件の場合はgroupIdも取得できるようになった。

{
  "responseHeader": {
    "status": 0,
    "QTime": 2,
    "params": {
      "q": "a:aopalliance AND v:1.0",
      "core": "",
      "indent": "off",
      "fl": "id,g,a,v,p,ec,timestamp,tags",
      "start": "",
      "sort": "score desc,timestamp desc,g asc,a asc,v desc",
      "rows": "20",
      "wt": "json",
      "version": "2.2"
    }
  },
  "response": {
    "numFound": 1,
    "start": 0,
    "docs": [
      {
        "id": "aopalliance:aopalliance:1.0",
        "g": "aopalliance",
        "a": "aopalliance",
        "v": "1.0",
        "p": "jar",
        "timestamp": 1451280333000,
        "ec": [
          "-sources.jar",
          ".jar",
          ".pom"
        ],
        "tags": [
          "alliance"
        ]
      }
    ]
  }
}
dekimasoondekimasoon

いよいよ最新バージョンと脆弱性報告数をとりたい。
と思ったらMaven Central RepositoryのAPIでは脆弱性報告数のような値はとれないらしい!

詰んだと思ったら、ライブラリの脆弱性を取得するツールがあるらしい。
神!初めて知った。

https://github.com/jeremylong/DependencyCheck

Macではbrewでインストールできる。
インストール後に、以下のようなコマンドでHTLMのレポートを作成してくれた。

> dependency-check --out . --scan "lib/*.jar"

なおフォーマットも指定できる。
JSONだと結果のサマリがなくHTMLのほうが見やすい(見方が悪いだけかも)

> dependency-check --out . --scan "lib/*.jar" --format JSON

レポートを整形して、以下のようなCSVを作成。
なんと最初に問題だったGroupIDもとれているっぽい。すごいぞ!

Package,Highest Severity,CVE Count
pkg:maven/org.postgresql/postgresql@42.2.16,CRITICAL,4
pkg:maven/org.apache.xmlgraphics/batik-bridge@1.7,CRITICAL,12

このCSVを元に、Maven Central RepositoryのAPIを呼び出して最新バージョンと最新バージョンの更新日を取得できれば良さそう。

最終的に書いた雑コードは以下。

import fs from 'fs'
import { parse } from 'csv-parse/sync'
import { Parser } from '@json2csv/plainjs';

const csv = fs.readFileSync('cve.csv', 'utf-8')
const packages = parse(csv, {
  columns: true
})

const summary = []
for (const p of packages) {
  const [_, groupId, artifactIdAndVersion] = p.Package.split('/')
  const [artifactId, version] = artifactIdAndVersion.split('@')
  const url = `https://search.maven.org/solrsearch/select?q=g:${groupId}+AND+a:${artifactId}`
  const resp = await fetch(url)
  const json = await resp.json()
  const detail = json.response.docs[0]
  const ts = detail ? new Date(detail.timestamp) : null
  const item = {
    name: p.Package,
    id: detail?.id,
    version,
    latestVersion: detail?.latestVersion,
    latestVersionTimestamp: ts ? `${ts.getFullYear()}-${String(ts.getMonth()+1).padStart(2, '0')}-${String(ts.getDate()).padStart(2, '0')}` : "",
    highestSeverity: p.HighestSeverity,
    cveCount: p.CVECount,
  }
  console.log(item)
  summary.push(item)
}

const parser = new Parser({
  delimiter: '\t'
});
const listCsv = parser.parse(summary);
fs.writeFileSync('output.csv', listCsv)

出力されるCSV(TSV)はこんな感じ。

name"	"id"	"version"	"latestVersion"	"latestVersionTimestamp"	"highestSeverity"	"cveCount"
"pkg:maven/aopalliance/aopalliance@1.0"	"aopalliance:aopalliance"	"1.0"	"1.0"	"2015-12-28"	""	"0"
"pkg:maven/org.ow2.asm/asm@7.3.1"	"org.ow2.asm:asm"	"7.3.1"	"9.6"	"2023-09-30"	""	"0"
このスクラップは2024/02/16にクローズされました