サイオステクノロジーは、OSS管理ツール「SCANOSS」の日本国内初の代理店です。
AI開発ツールの普及に伴い、ソースコードに含まれるOSSライセンスリスクへの対応が避けられなくなってきました。SCA(Software Composition Analysis)ツールを使うと、コードベースに混入したOSSコンポーネントを自動検出し、ライセンス違反や既知の脆弱性を可視化できます。
本記事では、SCAツール「SCANOSS」のPython CLI(scanoss-py)を使ったローカルスキャンの手順を紹介します。
この記事でわかること:
- scanoss-pyのインストールから基本スキャン、結果の読み方
- scanoss.jsonによる検出結果のチューニング方法
- CycloneDX / SPDX / CSV形式でのSBOM生成
- ScanCode Toolkit連携による依存関係スキャン(uv環境での回避策含む)
検証環境
| 項目 | 内容 |
|---|---|
| OS | Linux(WSL2 / devcontainer) |
| Python | 3.12 |
| パッケージ管理 | uv |
| scanoss-py | 1.54.2 |
| API Key | なし(匿名 / OSSKB パブリック API) |
| 検証日 | 2026年9月 |
API Keyなし(無料のOSSKBパブリックAPI)で検証しています。小〜中規模のプロジェクトであれば、API Keyなしでも一通り試せます。
スキャン対象には、検証用に用意した小さなPythonプロジェクトを使っています。自作コードのほかに、他所から貼ってきたコードの断片と、よくある定型の設定ファイルをわざと混ぜてあります。
インストール
pipまたはuvでインストールできます(PyPI: scanoss)。
# pip の場合
pip install scanoss
# uv の場合(dev依存として追加)
uv add --dev scanoss
インストール後、バージョンを確認します。
$ scanoss-py version
Version: 1.54.2
scanoss-py --helpでサブコマンド一覧を確認できます。よく使うものは以下です。
| サブコマンド | 説明 |
|---|---|
scan | ソースコードのスキャン |
dependencies(dep) | 依存関係のスキャン |
fingerprint | フィンガープリント(WFP)の生成 |
inspect | スキャン結果の点検(コピーレフト・未宣言の抽出、Markdownへの整形) |
convert | スキャン結果のフォーマット変換 |
file_count | ファイル数・サイズのサマリー |
このほかにresults / export / folder-scan / folder-hash / container-scan / crypto / component / deltaがあります。
スキャンして終わりではなく、出てきた結果を点検するinspectまで含めて1セットです。本記事はscanからconvertまでを扱います。
基本スキャン
ファイル数の確認
スキャン前に、対象ディレクトリのファイル構成を確認できます。
$ scanoss-py file_count ./sample-project
Found 7 files with a total size of 0.01 MB.
extension,count,size(MB)
.txt,1,0.00
.mjs,1,0.00
no_suffix,1,0.00
.py,3,0.00
.png,1,0.00
スキャンの実行
$ scanoss-py scan -o results.json ./sample-project
実行すると、対象ファイルのフィンガープリントを生成し、OSSKB(Open Source Knowledge Base)のナレッジベースと照合します。OSSKBには2億7,600万以上のURL[1]がインデックスされており、OSSファイル1億以上[1]、コード行数3兆行以上[1]をカバーしています。今回の7ファイルでは、匿名APIで10秒ほどで完了しました。
スキャン対象から外れるもの
file_countは7ファイルを数えましたが、結果に出てくるのは5ファイルです。3つが照合の前に落ちています。--debugを付けると理由が出ます。
Skipping file: requirements.txt (matches default skip ending: .txt)
Skipping file: .eslintrc.json (in hidden directory or is hidden file)
Skipping file: src/assets/logo.png (matches default skip ending: .png)
バイナリ(.png)とテキスト(.txt)は拡張子で自動的に除外されます。
注意したいのは3つ目で、ドットで始まるファイルは対象になりません。 .eslintrc.jsonは見られませんが、同じ設定を新形式のeslint.config.mjsに移すと照合対象に入ります。
スキャン結果の読み方
結果はJSON形式で出力されます。各ファイルごとにマッチ情報が記録されます。読みやすさのために主要フィールドだけを抜き出したものが以下です。
{
"src/date_helpers.py": [
{
"id": "snippet",
"component": "code-completion",
"vendor": "miguelvictor",
"version": "v0.0.3",
"file": "repositories/training/arrow-py/arrow/util.py",
"lines": "9-109",
"oss_lines": "6-106",
"matched": "87%",
"hits": 64,
"purl": ["pkg:github/miguelvictor/code-completion"],
"licenses": [],
"url": "https://github.com/miguelvictor/code-completion",
"status": "pending"
}
]
}
主要フィールドの意味は以下の通りです。
| フィールド | 説明 |
|---|---|
id | マッチ種別。none(マッチなし)/ snippet(部分一致)/ file(完全一致) |
component / vendor | 一致した相手のコンポーネント名と提供元 |
file | 一致した相手側のファイルパス |
matched | 一致率 |
hits | 一致した指紋の数 |
lines / oss_lines | 自分のコード / OSSコードの該当行範囲 |
purl | Package URL(コンポーネントの一意識別子) |
licenses | 検出されたライセンス情報 |
status | pending(未宣言)/ identified(宣言済み) |
実際の出力には上記以外のフィールドも含まれます。1件あたりに返るキーはcomponent / file / file_hash / file_url / hits / id / latest / licenses / lines / matched / oss_lines / purl / release_date / server / source_hash / status / url / url_hash / vendor / versionです。
マッチしなかったファイルも、結果には1エントリとして残ります。
{
"src/utils.py": [
{ "id": "none", "server": { "elapsed": "0.001014s", "kb_version": { "daily": "26.09.04" } } }
]
}
「何も無かった」は空ではなくid: noneとして出ます。 件数を数えるときはここを除く必要があります。
今回の検証結果
| 項目 | 件数 |
|---|---|
| スキャン対象 | 7ファイル |
| 照合前に除外 | 3 |
| マッチなし(id=none) | 3 |
| スニペットマッチ(id=snippet) | 1 |
| フルファイルマッチ(id=file) | 1 |
検出は2件でした。
| ファイル | 種別 | 一致した相手 | 一致率 |
|---|---|---|---|
src/date_helpers.py | snippet | code-completion | 87% |
eslint.config.mjs | file | Dash-Desktop | 100% |
この2件は性質がまったく違います。
src/date_helpers.pyは、実際に他所からコードを貼っています。一致は本物です。ただし貼ってきた元と、返ってきたコンポーネント名は一致しません。ここに貼ったのは日付ライブラリarrowのコードですが、SCANOSSが返したのはcode-completionという別のリポジトリでした。fileフィールドを見るとrepositories/training/arrow-py/arrow/util.pyとなっており、同じコードを取り込んだ第三者のリポジトリのほうが先に当たったことがわかります。
コンポーネント名を「これが元のOSSだ」と読むと取り違えます。
eslint.config.mjsのほうは、こちらが用意した定型の設定ファイルです。create-next-appが生成する既定の内容をそのまま置いただけですが、まったく無関係なDash-Desktopというプロジェクトに、ファイル丸ごと100%で一致しました。同じ内容のファイルが世の中に大量にあるためです。
scanoss.jsonで検出結果をチューニングする
検出をどう扱うかはscanoss.jsonで指定できます(公式ドキュメント)。
設定ファイルの構成
トップレベルに置けるのはself / settings / bomの3つです。
{
"settings": {
"skip": {
"patterns": {
"scanning": ["**/__pycache__/**", "**/node_modules/**"]
}
}
},
"bom": {
"include": [
{
"purl": "pkg:github/miguelvictor/code-completion",
"comment": "宣言済みコンポーネントとして登録"
}
],
"remove": [
{
"path": "eslint.config.mjs",
"comment": "定型ファイルが無関係なプロジェクトに一致したので落とす"
}
]
}
}
| セクション | 効果 |
|---|---|
settings.skip.patterns.scanning | 指定したファイルをスキャンしない(指紋も作らず、APIにも送らない) |
bom.include | スキャン時に追加コンテキストとしてPURLをAPIに送信する[2] |
bom.remove | スキャン完了後に、結果ファイルからそのファイルの結果を取り除く[2] |
bom.replace | スキャン完了後に、コンポーネントを指定したものへ差し替える[2] |
skipはトップレベルではなくsettingsの下に置きます。 トップレベルに書いたskipは読み飛ばされますが、エラーも警告も出ません。Loaded scan settings from: ...と成功だけが報告されて、除外が効いていない状態になります。効いているか確かめたいときは--debugを付けて、Skipping file: ...(matches custom pattern)が出るかを見てください。
settings.skip.patternsの下はscanning / fingerprinting / dependenciesに分かれています。スキャン対象を絞りたいときはscanningです。
include / remove / replace の使い分け
3つは似ていますが、検出をどう判断したかで使い分けます。
| 検出をどう判断したか | 使うもの |
|---|---|
| 一致も帰属も正しい。使っていることを認める | bom.include |
| 一致していない(定型ファイルがたまたま当たった等) | bom.remove |
| 一致は本物だが、帰属先のコンポーネントが違う | bom.replace |
今回の2件はきれいに分かれます。eslint.config.mjsの100%一致は「一致していない」のでremove、src/date_helpers.pyは一致自体は本物なので、正しい帰属がわかっているならreplaceです。
replaceが受け持つのは誤検出ではなく帰属の誤りです。一致した箇所そのものは本物なので、matchedやlinesはそのまま残り、差し替わるのはコンポーネント名・PURL・ライセンスの側だけです。
"replace": [
{
"path": "src/date_helpers.py",
"purl": "pkg:github/miguelvictor/code-completion",
"replace_with": "pkg:pypi/arrow",
"comment": "一致は本物だが、帰属先が違っていたので直す"
}
]
設定を適用してスキャン
$ scanoss-py scan --settings /absolute/path/to/scanoss.json -o results.json ./sample-project
設定適用前後の比較結果です。
| ファイル | Before | After |
|---|---|---|
src/date_helpers.py | code-completion 87%(pending) | code-completion 87%(identified) |
eslint.config.mjs | Dash-Desktop 100%(pending) | 結果から消える |
scanoss.jsonの注意点
--settingsには絶対パスを指定するのが安全
相対パスを渡すと、次の順で候補を探し、最初に見つかったものが使われます。
- スキャン対象ディレクトリからの相対パス
- カレントディレクトリからの相対パス
- 渡した文字列をそのまま
同じ名前の設定ファイルが両方にあると、意図しないほうが読まれます。パスの先頭を./にすると1が飛ばされ、カレントディレクトリ基準になります。
# 曖昧: スキャン対象側 → カレントの順に探す
scanoss-py scan --settings path/to/scanoss.json ./sample-project
# 明確: 絶対パスなら候補が1つに決まる
scanoss-py scan --settings /home/user/scanoss.json ./sample-project
絶対パスを勧める理由は「相対パスだと動かないから」ではなく、どれが読まれたか迷わずに済むからです。
bom.removeが落とすのは「そのファイルの結果」
pathとpurlはどちらか一方だけでも書けます。両方書くといちばん限定的なルールになり、purlだけなら全パスから、pathだけならそのファイルの検出を種類を問わず落とします。
# このファイルの、このコンポーネントだけ
{ "path": "src/app.py", "purl": "pkg:github/owner/repo" }
# ファイルごと(purl を知らなくてもよい)
{ "path": "eslint.config.mjs" }
pathはディレクトリにも当たります。 "src"と書くとsrc/配下の検出がすべて消えます。誤検出を1件消すつもりでディレクトリを書くと、そのツリーが丸ごと静かになります。
落ちるのはPURL単位ではなく、そのファイルの結果ごとです。 id: noneとしても残りません。「そのファイルを見て、何も無かった」という記録も一緒に消えます。
removeは送ってから捨てているだけです。 指紋は作られてAPIにも送られ、返ってきた結果を手元で落としています。ファイル自体を外部に出したくないならsettings.skip.patterns.scanningを使ってください。
bom.includeはマッチを消すのではなく「宣言済み」にする
includeに登録しても、スキャン結果からマッチ自体が消えるわけではありません。該当コンポーネントのstatusがpendingからidentifiedに変わります。登録したPURLはスキャン時にコンテキストとして送られるため、スキャン結果が次候補のコンポーネントに変化する場合もあります。
SBOM生成
スキャン結果を業界標準のSBOM形式に変換できます。
# CycloneDX
scanoss-py convert -i results.json -f cyclonedx -o sbom.cdx.json
# SPDX-Lite
scanoss-py convert -i results.json -f spdxlite -o sbom.spdx.json
# CSV
scanoss-py convert -i results.json -f csv -o results.csv
指定できる形式と用途は以下の通りです。-fを省略した場合の既定はspdxliteです。
| 形式 | 用途 |
|---|---|
cyclonedx | Dependency Track等のSBOM管理ツールへの連携 |
spdxlite | ライセンスコンプライアンス管理(業界標準仕様) |
csv | Excelやスプレッドシートでの手動レビュー |
glc-codequality | GitLab Code Quality形式でのレポート |
注意点として、CycloneDX出力は拡張子を.xmlにしても実際の出力はJSON形式です。XML形式での出力が必要な場合は別途変換が必要です。
依存関係スキャン
scanoss-pyは、requirements.txtやpyproject.toml等から依存パッケージを検出する機能も備えています。この機能にはScanCode Toolkitが必要です。
セットアップ
# 1. scancode-toolkit のインストール
pip install scancode-toolkit
# または
uv add --dev scancode-toolkit
# 2. libgomp1 のインストール(Debian系のslimイメージの場合)
sudo apt-get install -y libgomp1
# 3. 動作確認
scancode --version
# → ScanCode version: 32.5.0
uv環境での注意点
ScanCode Toolkitはuv.lockに対応していません(GitHub Issue #4501)。サポート対象のパッケージマニフェスト形式は公式ドキュメントで確認できます(requirements.txt、pyproject.toml、poetry.lock、Pipfile.lock等は対応済み)。
uv環境で依存関係をスキャンする場合、uv exportでrequirements.txtに変換する必要があります。
uv export --format requirements-txt --no-hashes --no-emit-workspace -o requirements.txt
なお、--no-devオプションを付けると、直接依存がすべてdev依存のプロジェクトでは空ファイルになります。必要に応じてオプションを調整してください。
依存関係スキャンの実行
# dep サブコマンド(API送信なし、ローカルのみ)
scanoss-py dep -o dependencies.json ./sample-project
# scan --dependencies(通常スキャン + 依存関係スキャン)
scanoss-py scan --dependencies -o results.json ./sample-project
ディレクトリを指定すると、ScanCode Toolkitが配下のrequirements.txtやpyproject.toml等のマニフェストファイルを自動検出してパースします。個別にファイルを指定する必要はありません。
--sc-commandでscancodeの場所を指定できますが、省略できます。省略した場合はscancodeという名前でPATHから探されます。仮想環境を有効化していてPATHが通っているなら、指定は要りません。PATHに無い場合や複数の環境を使い分けている場合に、絶対パスで明示します。
scanoss-py dep --sc-command /path/to/.venv/bin/scancode -o dependencies.json ./sample-project
--sc-commandに渡すのは実行ファイルのパスです。uv run scancodeのような形式は内部のsubprocess呼び出しの都合で使えません。
depサブコマンドとscan --dependenciesの違いは以下の通りです。
dep | scan --dependencies | |
|---|---|---|
| SCANOSS API送信 | なし | あり |
| ライセンス情報の付与 | なし | あり |
| 用途 | 事前確認・デバッグ | フルSBOM生成 |
depが返すのはマニフェストから抽出したPURLの一覧だけです。
{
"files": [
{
"file": "requirements.txt",
"purls": [ { "purl": "pkg:pypi/certifi" }, { "purl": "pkg:pypi/mysqlclient" } ]
}
]
}
バージョンもライセンスも付きません。そこまで必要ならscan --dependenciesを使います。
コード側と依存側は同じファイルに同居する
scan --dependenciesを使うと、1本のresults.jsonにコード側の照合結果と依存側の結果が両方入ります。ただし構造が違います。
| キー | id | 中身 |
|---|---|---|
src/date_helpers.py | snippet | コードの一致情報 |
eslint.config.mjs | file | コードの一致情報 |
requirements.txt | dependency | dependencies配列に依存の一覧 |
読むときはidを見て、どちらの検出かを判別することになります。
フィンガープリントの活用
fingerprintサブコマンドを使うと、スキャンとAPI送信を分離できます。
# フィンガープリントの生成(オフラインでもOK)
scanoss-py fingerprint -o fingerprint.wfp ./sample-project
# 後からスキャン(ネットワーク接続時)
scanoss-py scan --wfp fingerprint.wfp -o results.json
ネットワークに接続できない環境でフィンガープリントだけ先に生成し、接続時にスキャンを実行するワークフローに活用できます。
運用上のポイント
推奨ワークフロー
- 初回スキャンを設定なしで実行し、検出結果を確認する
- 検出を1件ずつ見て、一致していないのか / 帰属が違うのか / 認めるのかを決める
- 判断をscanoss.jsonに落とし、再スキャンして反映を確認する
- 結果に問題がなければ、SBOMを生成する
- scanoss.jsonをリポジトリに含め、CI/CDでの継続的スキャンに移行する
デフォルトの設定ファイル探索
scanoss-pyは、--settingsを指定しなくてもscanoss.jsonを自動的に探します。探す順はスキャン対象ディレクトリ直下、カレントディレクトリ、の順です。リポジトリのルートに置いておけば、オプション指定なしでチューニング済みのスキャンを実行できます。
カレントディレクトリまで探しに行くので、意図しない設定を拾うことがあります。 スキャン対象にscanoss.jsonが無くても、コマンドを打った場所にあれば読まれます。素の状態を確認したいときは--skip-settings-fileを付けてください。
API Keyなしとありの違い
本記事の検証はAPI Keyなし(OSSKBパブリックAPI)で実施しています。小〜中規模のプロジェクトであればAPI Keyなしでも試せますが、本番運用ではAPI Keyの利用を検討してください。
| 項目 | API Keyなし | API Keyあり |
|---|---|---|
| 基本スキャン | 利用可能 | 利用可能 |
| 可用性保証 | なし | SLA保証あり |
| スループット保証 | なし | 保証あり |
| 1件あたりに返る情報 | 20フィールド | 28フィールド |
返ってくる情報量が違います。 API Keyありでは、匿名では返らないvulnerabilities(既知の脆弱性)・cryptography(使われている暗号アルゴリズム)・health(リポジトリの活動状況)・quality・copyrights・provenance・dependencies・url_statsが付きます。ライセンス以外の判断材料が要るなら、ここが効いてきます。
API Keyありでスキャンする場合は、--keyオプションを指定します。
scanoss-py scan --key YOUR_API_KEY -o results.json ./sample-project
API Keyの取得にはSCANOSSとの契約が必要です。CI/CD環境で利用する場合は、GitHub Actionsのsecrets.SCANOSS_API_KEYのようにシークレットとして管理してください。
具体的なレート制限値は公式ドキュメントに記載されていないため、大規模プロジェクトでの利用時はSCANOSSへの問い合わせを推奨します。
まとめ
scanoss-pyを使ったローカルスキャンの手順を一通り紹介しました。
| 機能 | ポイント |
|---|---|
| 基本スキャン | API Keyなしで即実行可能。バイナリとドット始まりのファイルは対象外 |
| 結果の読み方 | コンポーネント名は「元のOSS」とは限らない。定型ファイルは無関係な相手に丸ごと一致する |
| scanoss.json | トップレベルはself / settings / bom。検出の判断に応じてinclude / remove / replaceを使い分ける |
| SBOM生成 | CycloneDX / SPDX / CSV / GitLab Code Qualityの4形式に対応 |
| 依存関係スキャン | ScanCode Toolkitが必要。uv.lockは未対応 |
ローカルスキャンで動作を確認した後は、CI/CDパイプラインへの統合を検討することで、プルリクエスト単位での自動チェックが可能になります。GitHub Actionsでの構成は「SCANOSS GitHub Actionsでコードスキャンを自動化」で解説しています。


