セキュリティ

VirusTotal APIで検体判定を自動化する実装手順:レート制限と社内ファイルの扱い

VirusTotal APIで検体判定を自動化する実装手順:レート制限と社内ファイルの扱い

ブラウザでファイルをドラッグして70社ぶんのスキャン結果を見るところまでは、誰でも5分で終わります。手が止まるのはその次です。社内のアップロード機能に判定を挟みたい、CIで作った成果物を毎回チェックしたい、SOCに上がったハッシュを自動で引きたい。そこでPublic APIの毎分4件という上限にぶつかり、さらに「この検体を外に出していいのか」という別種の問題が出てきます。この記事では、x-apikey ヘッダの叩き方から last_analysis_stats の読み方、レート制限下で回すバックオフ実装、そしてアップロードが業界パートナーへ共有される規約とその回避策までを、公式ドキュメントの記載とコードで示します。

まとめ|VirusTotal APIを社内システムへ組み込む前に確定させる4点

先に結論から並べます。ひとつ目は流量です。Public APIとPremium APIの比較ページには「500 requests per day and a rate of 4 requests per minute」と明記されています。1日500件。社内のファイルサーバーやメール添付を全件通す設計は、この2行で成立しません。設計の出発点は「何を通さないか」の側にあります。

ふたつ目は検体の行き先です。VirusTotalへファイルを投稿すると、その中身はコーパスへ保存され、アンチマルウェア業界のパートナーへ共有されます。顧客から預かった見積書や社内の設計書を、判定のためにアップロードしてよいかは別の判断になります。共有を止めるには有償のPrivate Scanningが必要で、その代わりにアンチウイルスの判定結果は返ってきません。

3つ目は判定ロジックです。Fileオブジェクトの属性一覧を見ると last_analysis_stats は8フィールドに分かれていて、単純な「陽性何件」ではありません。timeout や type-unsupported を母数に混ぜたまま比率を出すと、判定が実態から外れます。

4つ目は実装の型です。ハッシュ照会を先に置き、未登録のときだけアップロードへ落とす。429が返ったら指数バックオフで待つ。同じハッシュは自前でキャッシュする。この3点を入れておけば、毎分4件の枠でも実務は回ります。以降の章で、そのコードと判断基準を順に示します。

VirusTotal APIの構造とPublic APIで実際に返ってくる判定値の内訳

API v3のベースURLは https://www.virustotal.com/api/v3 で、認証はヘッダ1本だけです。OAuthのフローもトークン更新もありません。まずはここを確認しておきます。

APIキー取得からcurlで最初のレポートを取得するまでの手順

APIキーはアカウント作成後、ユーザーメニューのAPIキー画面からコピーします。無料アカウントでもキーは即発行されます。認証はリクエストヘッダに x-apikey を載せるだけで、クエリパラメータでの渡し方は使いません。

ファイルレポート取得のリファレンスによると、エンドポイントは GET /files/{id} で、id にはSHA-256・SHA-1・MD5のいずれかを渡せます。下のコマンドで使っているハッシュはEICARテストファイルのMD5です。手元の本物の検体を使わずに動作を確かめられます。

# 1) APIキーは環境変数へ。ソースにもリポジトリにも直書きしない
export VT_API_KEY="your-api-key"

# 2) SHA-256やMD5でレポートを引く(アップロードしないので検体は外に出ない)
curl -s --request GET \
  --url "https://www.virustotal.com/api/v3/files/44d88612fea8a8f36de82e1278abb02f" \
  --header "x-apikey: $VT_API_KEY" | jq ".data.attributes.last_analysis_stats"

# 期待される戻り値の例(エンジン数は実行時点で変わる)
# { "malicious": 63, "suspicious": 0, "undetected": 3, "harmless": 0,
#   "timeout": 0, "confirmed-timeout": 0, "failure": 0, "type-unsupported": 11 }

ここで押さえておきたいのは、この照会がアップロードではないという点です。ハッシュを送って既存のレポートを引くだけなので、ファイル本体はVirusTotalへ渡りません。社内システムに組み込む実装では、この経路を主に使います。

last_analysis_statsの8フィールドと判定閾値の切り方

返ってきた数値をそのまま「陽性率」にすると判定を誤ります。8フィールドのうち4つは、そもそもエンジンが結論を出せなかったことを示すからです。

フィールド 意味 実装での扱い
malicious 悪性と判定したエンジン数 遮断判断の主軸
suspicious 疑いと判定したエンジン数 maliciousに合算する
harmless 無害と判定したエンジン数 単独では根拠にしない
undetected 検知しなかったエンジン数 未知の検体で大きくなる
timeout 時間内に終わらなかった数 母数から除く
confirmed-timeout 確定的な時間切れの数 母数から除く
failure 解析が失敗した数 母数から除く
type-unsupported 形式に非対応のエンジン数 母数から除く

実務では比率を出さず、件数で切るほうが安定します。malicious と suspicious の合計が2以上なら遮断、1なら人が見るキューへ、0なら通す。この3段構えにする理由は、検知1件だけのケースに誤検知がまとまって現れるからです。加えて reputation はコミュニティ投票の合算値であって、エンジンの判定とは別系統の数字です。自動判定の条件に混ぜないでください。

エンジン名と検知名の並びを見るときは last_analysis_results を開きます。ここに出る検知名は各社の命名規則で、同じ検体でも表記が揃いません。トロイの木馬とは何かを実装目線で整理した記事で挙げた分類の考え方が、検知名から実際の挙動を推測するときの手がかりになります。

URLとドメイン・IPを調べるときのbase64識別子の作り方

ファイルはハッシュで引けますが、URLは少し面倒です。URLレポート取得のリファレンスには、GET /urls/{id} の id は「base64 representation of URL to scan (w/o padding)」と書かれています。パディングの = を落とすところが要点です。Pythonなら base64.urlsafe_b64encode(url.encode()).decode().strip("=") の1行で作れます。

ドメインとIPアドレスはエンコードが不要で、/domains/{domain} と /ip_addresses/{ip} にそのまま渡せます。SOCへ上がったアラートからIoCを引くワークフローでは、ハッシュ・URL・ドメイン・IPの4系統をこの違いだけ意識して分岐させれば足ります。

Pythonで1分4リクエスト制限を守りながら一括判定する実装

ここからが本題です。毎分4件という枠は、素朴に for で回すと即座に破れます。クォータ消費の解説ページによると、1回のAPI呼び出しが1リクエストぶんの消費で、複数ハッシュをまとめて投げる検索でもハッシュ1個につき1消費です。まとめて投げても得はしません。

429を前提にした指数バックオフとハッシュ先行照会の実装コード

クォータを使い切ると、API v3は429を返します。リトライ側で待つ設計にしておかないと、日中の1回のバッチで翌日ぶんまで潰れることになります。次のコードは、ハッシュ照会を先に置き、404(未登録)を「アップロード判断へ回す」フラグとして扱う形です。

import os
import time
import requests

API = "https://www.virustotal.com/api/v3"
HEADERS = {"x-apikey": os.environ["VT_API_KEY"]}
# Public APIは毎分4件が上限。60秒 ÷ 4 に余裕を足して15.5秒間隔で回す
MIN_INTERVAL = 15.5

def get_file_report(sha256, retry=4):
    """ハッシュ照会のみ。ファイル本体はVirusTotalへ送らない"""
    for attempt in range(retry):
        r = requests.get(f"{API}/files/{sha256}", headers=HEADERS, timeout=30)
        if r.status_code == 200:
            return r.json()["data"]["attributes"]
        if r.status_code == 404:
            return None                      # 未登録。アップロードするか人が見るかを別途判断
        if r.status_code == 429:
            time.sleep(MIN_INTERVAL * (2 ** attempt))
            continue
        r.raise_for_status()
    raise RuntimeError("VirusTotalのクォータを消費し切りました")

def verdict(stats):
    """timeout系とtype-unsupportedを母数に混ぜず、件数で3段に切る"""
    hit = stats.get("malicious", 0) + stats.get("suspicious", 0)
    if hit == 0:
        return "allow"
    if hit == 1:
        return "review"                      # 検知1件は誤検知が混じるので人が見る
    return "block"

def scan_hashes(hashes, cache):
    for h in hashes:
        if h in cache:                       # 同じハッシュは二度引かない
            yield h, cache[h]
            continue
        attrs = get_file_report(h)
        if attrs is None:
            cache[h] = "unknown"
        else:
            cache[h] = verdict(attrs["last_analysis_stats"])
        yield h, cache[h]
        time.sleep(MIN_INTERVAL)

キャッシュを外部ストアに置く構成が有効です。社内で流通するファイルは同一ハッシュの再登場が多く、実装したキャッシュがそのまま流量削減になります。判定結果に有効期限を持たせて、悪性判定は長め、未検知は短めに切っておくと運用が楽になります。

32MBを超えるファイルでupload_urlを取得する分岐の書き方

未登録のファイルをどうしてもスキャンさせる場合は、アップロードのエンドポイントを使います。ファイルアップロードのリファレンスによると POST /files は multipart/form-data の file フィールドを受け取り、戻り値は解析ジョブの id です。レポートそのものは返りません。/analyses/{id} をポーリングして待ちます。

サイズの境界は32MBです。32MBを超えて650MBまでのファイルは /files/upload_url で専用のアップロード先URLを取得し、そこへPOSTする2段構えになります。同じ関数のまま大きいファイルを投げると失敗するので、サイズを見て分岐させてください。ZIPにパスワードが掛かっている場合は password フィールドを添えられますが、非ZIP・複数ファイル・パスワード誤りではいずれも400が返ります。

なお、アップロード自体はクォータを消費しません。消費するのはその後のレポート取得です。ここを勘違いすると流量の見積もりがずれます。

vt-cliでスキャン結果をCIのジョブから確認するコマンド例

CIから使うだけなら、Pythonを書かずに公式CLIで済みます。vt-cliのリポジトリはApache License 2.0で公開されていて、実測時点の最新リリースタグは1.3.1(2026年5月公開)でした。1.3系として扱えば当面は問題ありません。

# インストールと初期設定(キーは ~/.vt.toml に保存される)
brew install virustotal-cli
vt init

# CIのジョブから成果物のハッシュだけを照会する(アップロードしない)
sha256sum dist/app.bin | cut -d " " -f1 \
  | vt file - --include=last_analysis_stats

# 依存パッケージのアーカイブを一括で確認する
cat build/hashes.txt | vt file - --format=json

# 検体をアップロードして解析させる場合(外部へ渡る点に注意)
vt scan file dist/app.bin

CIに入れるなら、置き場所は「依存パッケージを取得した直後」です。ビルド後の自社バイナリを引いても未登録で返るだけで、得られる情報がありません。第三者から取り込んだアーカイブのハッシュを照会するほうが、検知に当たる確率が上がります。この設計はサプライチェーンセキュリティを開発工程へ組み込む実装判断で整理した考え方と同じ線に乗ります。

社内ファイルをPublic APIへ投げてはいけない理由と代替手段

技術的に動くことと、業務で使ってよいことは別です。ここを飛ばして本番へ入れると、後から止めるほうが高くつきます。

アップロードした検体が業界パートナーへ共有される範囲と規約の記載

VirusTotalへサンプルを投稿すると、その内容はコーパスへ保存され、アンチマルウェアおよびセキュリティ業界のパートナーと共有されます。パートナー側は契約で用途を内部のセキュリティ目的に限定されていますが、「自社の外へ出ない」わけではありません。研究者や政府機関、脅威インテリジェンスを扱う組織も参照者に含まれます。

問題になるのは、判定したいファイルの中身です。顧客から預かった申込書、社内の見積書、設計書。これらは検体としての価値はゼロで、漏れたときの損害だけが残ります。しかもクォータ消費の解説ページにある通り、新規ファイルのアップロードそれ自体はリクエストを消費しません。投げるコストがゼロだから、実装時にうっかり通してしまう構造になっています。無料で使えるという理由で業務フローに入れる前に、外へ出るのが何かを確認してください。

Private Scanningで共有を止める場合の保持期間と機能の欠落

共有そのものを止める選択肢がPrivate Scanningです。Private Scanningの解説ページには、提出したファイルとURLは第三者へ共有されず、レポートは自組織のグループ内でのみ可視だと書かれています。既定の保持期間は概ね24時間で、期限を過ぎるとプライベートバケットから削除されます。手動削除も可能です。

ただし引き換えがあります。同ページには「private analyses won’t contain antivirus verdicts」と明記されていて、マルチアンチウイルスの判定は付きません。返ってくるのは静的解析・動的解析・YARAやSIGMAによる検出、脅威インテリジェンスの付加情報です。「70社のうち何社が悪性と言ったか」で判断する設計のまま切り替えると、判定ロジックが空になります。しかも有償で、権限の付与が別途必要です。

動的解析の結果だけを見て運用する形になるため、事実上は隔離環境での実行結果を読む作業に近づきます。セキュリティにおけるサンドボックスの役割と限界を整理した記事で触れた「検知回避を仕込まれた検体は静かに振る舞う」という制約は、そのままこちらにも当てはまります。

ハッシュ照会だけで運用する社内向けクライアントの設計方針と実装

実務で最も収まりがよいのは、アップロードを一切しない構成です。ファイル本体はハッシュ計算まで自社内で完結させ、外へ出すのは64文字の16進文字列だけにする。この形なら情報漏えいの経路が閉じます。

未登録(404)が返った場合の扱いを、あらかじめ決めておいてください。選択肢は3つあります。ひとつは人が見るキューへ回す。ふたつ目は自前のサンドボックスへ流す。3つ目は無条件に隔離する。「未登録だから安全」と解釈して通す運用だけは避けます。新しいマルウェアは、必ず一度は未登録の状態を通過するからです。

アップロード機能を残す場合でも、対象を絞ります。実行可能形式とアーカイブに限定し、業務文書は対象外にする。この線引きをコード側の許可リストで持っておくと、運用者の判断に依存しなくなります。

VirusTotalを採用しない場面と自社実装へ切り替える判断基準

ここまでの制約を踏まえて、どこで採用し、どこで見送るかを言い切ります。

検知エンジン数の多数決をそのまま最終判定に使えない3つの理由

「63社が悪性と判定」という数字は説得力がありますが、これを閾値の根拠にしてはいけません。理由は3つあります。

1つ目は、エンジンの独立性が担保されていない点です。複数のベンダーが同じ検知エンジンやシグネチャを内部で共有しているため、票が独立に増えているわけではありません。2つ目は、静的シグネチャに寄ったエンジンが多く、圧縮や難読化を施した既知のマルウェアで検知数が大きく落ちることです。3つ目は逆方向で、自社ビルドのインストーラやパッカーで固めたバイナリに、複数エンジンが揃って誤検知を出す挙動があります。

この3つが同時に効くため、比率での判定は破綻します。件数で3段に切り、検知1件は人が見る。それが実装として落ち着くところです。判定を自動遮断に繋ぐなら、検知したエンジン名と検知名を必ずログへ残して、後から誤検知を切り分けられるようにしておきます。

Public APIの500件制限で回らなくなる社内規模の分岐点

1日500件。この数字を業務量に当てて考えます。キャッシュヒット率を仮に7割と置くと、実際に処理できるのは1日1,600件前後です。従業員100名規模で、Webアップロード機能を持つ社内システム1本を守る用途なら収まります。

収まらないのは次の3つです。メールゲートウェイの添付を全件通す用途。ファイルサーバー全体の定期棚卸し。エンドポイントから上がる実行ログのハッシュを毎回引く用途。いずれも件数が桁で変わるため、Public APIでは最初から不成立です。この段階に来ているなら、Premium APIの見積もりを取るか、判定の主体をEDRなど別の製品へ移すほうが早いです。

もう一点、無料キーを複数作って回避する運用は取らないでください。規約上の問題に加えて、キー単位でのレート制御が入っているため、実装が複雑になるだけで安定しません。

有償プランと自前サンドボックス構築のどちらを選ぶかの判断基準

結論から言います。「既知のマルウェアを弾きたい」ならVirusTotal。「自社に固有の未知の検体を分析したい」なら自前の解析環境です。両者は代替関係にありません。

VirusTotalの強みは、70社ぶんのシグネチャと過去の投稿履歴という他社が持てない蓄積にあります。これを自前で作ることは不可能です。逆に、外に出せないファイルを日常的に扱う業務では、Private Scanningを買うよりも隔離環境を自社に置くほうが説明が簡潔です。監査で「顧客データをどこへ送っているか」を問われたとき、送っていないと答えられる構成には価値があります。

判断が分かれるのは中間層です。月に数百件の未知検体があり、外部送信の可否が案件ごとに違う。この場合は、ハッシュ照会をVirusTotalに任せ、未登録のものだけ自前環境へ回す2段構成が現実的です。既存システムへのAPI組み込みや、判定結果を業務フローへ流し込む部分の設計は、外部サービス連携の設計経験がある事業者に相談したほうが手戻りが減ります。当社ではAPI開発・システム連携として、この種の外部API組み込みと社内システム側の受け口の実装を請けています。

よくある質問

VirusTotalをAPIで組み込むときに実際に聞かれることを5つまとめました。

VirusTotalは無料で使えますか?

ブラウザからの利用とPublic APIは無料です。ただしPublic APIには毎分4リクエスト・1日500リクエストの上限があり、ファイルのダウンロードや条件を組んだ逆引き検索、動的解析の詳細、投稿元の国や流通状況といった情報はPremium限定です。個人の調査や小規模な社内システム1本なら無料枠で足りますが、ゲートウェイ全件やファイルサーバー棚卸しには届きません。商用サービスの中核機能として組み込む場合も、規約の確認と有償プランの検討が必要になります。

VirusTotalにアップロードしたファイルは他人に見られますか?

見られます。投稿したサンプルはコーパスへ保存され、アンチマルウェアおよびセキュリティ業界のパートナーと共有されます。パートナーは契約で用途を内部のセキュリティ目的に限定されますが、自社の外に出ないという意味ではありません。共有を止めたい場合は有償のPrivate Scanningを使い、その代わりアンチウイルス判定は付かなくなります。顧客データや社内文書を判定したい用途では、アップロードせずハッシュ照会だけで済ませる設計を選んでください。

VirusTotal APIのキーはどこで取得しますか?

アカウントを作成してサインインし、右上のユーザーメニューからAPIキーの画面を開くとコピーできます。無料アカウントでも即時に発行されます。認証はリクエストヘッダに x-apikey を載せる方式で、クエリパラメータでは渡しません。キーはソースコードやリポジトリに直書きせず、環境変数かシークレット管理サービスに置いてください。公式CLIの vt-cli を使う場合は vt init で対話的に登録され、~/.vt.toml に保存されます。

検知が1件だけ出た場合はマルウェアと判断すべきですか?

自動で遮断する根拠にはしないでください。検知1件のケースには誤検知がまとまって現れます。自社ビルドのインストーラやパッカーで固めたバイナリで、特定のエンジンだけが反応する挙動は珍しくありません。実装としては malicious と suspicious の合計が2以上で遮断、1なら人が確認するキューへ回す3段構えが安定します。確認するときは last_analysis_results でどのエンジンが何という検知名を出したかまで見て、既知のファミリー名かヒューリスティック由来の汎用名かを区別します。

社内のファイルサーバー全体をVirusTotalでスキャンできますか?

Public APIでは不可能です。1日500リクエストの上限があるため、数万件規模の棚卸しは日数で割っても終わりません。加えて、業務文書をアップロードする構成は情報漏えいの経路そのものになります。ファイルサーバー全体を対象にするなら、エンドポイント保護製品やファイルサーバー用のアンチウイルスを使うのが筋です。VirusTotalは「特定のファイルが既知のマルウェアか」をピンポイントで確認する用途に向いていて、面の防御を担う製品ではありません。

関連記事

お気に入りに入れた記事の一覧

この記事は以下の記事からリンクされています

資料請求

今日のトレンド記事 直近 24 時間で、いつもより多く読まれている記事

  1. 2026.07.21 テックブログ Apache Tomcatの脆弱性一覧【2026年10月】最新版11.0.27・10.1.61・9.0.123とCVE-2026-59083ほかの影響確認・対応手順
  2. 2026.10.21 テックブログ NEC委託先の不正アクセスとは|顧客システム暗号化・情報漏えいの経緯と委託元が点検する正規アカウント
  3. 2026.10.21 テックブログ ふるさとチョイス不正アクセスで445万件流出:対象確認・パスワード変更と再開時期
  4. 2026.10.22 テックブログ Tomcatバージョン一覧とサポート期限【2026年10月】9.0系は2027年3月末で終了・9.1系と11.0系への移行判断
  5. 2024.07.05 コラム XAMPPの脆弱性とは?危険性とCVE-2024-4577への対策をわかりやすく解説【2026年10月】

RELATED POSTS 関連記事

目次