AIで完結!SaaS導入のセキュリティチェック
SREホールディングス株式会社 でデータサイエンティストをやっている井上です。
生成AIの登場は、エンジニアのコーディング速度を劇的に加速させました。
例えば、簡単な関数の自動生成からアーキテクチャのたたき台作成まで、生成AIの導入前と比べてコードを書く時間は半分以下に短縮されています。また、単なるコーディング支援にとどまらず、PDCAサイクルの高速化も可能となり、個人やチームの生産性は新たなフェーズに突入しています。
生成AIの活用は、開発現場における新たな差別化要因ともいえるものです。
このスピードアップを実現するために、どのような環境やツールが求められるでしょうか。
ポイントの1つがGitHub CopilotやCursorといった「即利用可能なSaaSサービス」の活用です。
SaaSを安全に導入するためには常にセキュリティチェックは非常に重要です。
しかし、セキュリティチェックに必要な情報を集めるには手間がかかります。
本記事では、手間のかかる作業をAIに任せ、開発のスピードと信頼性を両立させる取り組みについて紹介します。
対象読者
- これから自社にSaaSの導入を検討している方
- 社内でSaaS導入のセキュリティレビューを行う方
扱う内容
- OpenAI Agents SDKを使用したレビューのシミュレーション
本記事のモチベーション
この記事を書こうと思ったきっかけは、自社でSaaS導入に向けたセキュリティレビューを行っていた際に感じた疑問からでした。
セキュリティ関連の質問に対して、どのような情報源をもとに回答するべきかを調べているうちに、「これ、いちいち人手で検索するよりも、AIに任せたほうが早くて的確なのでは?」と思うようになったのです。
さらに、実際にOpenAIのPython APIを使って想定される質問に対して回答させてみたところ、「あれ、これ意外と使えるかも?」と手応えを感じました。
そこから、「セキュリティチェック業務の一部をAIに置き換えられないか?」という関心が生まれ、本記事の執筆に至っています。
以下、OpenAI Python APIで行ったデモコード
from typing import List
from pydantic import BaseModel, Field
from agents import Agent, Runner, WebSearchTool
class SecurityAnswer(BaseModel):
question: str = Field(..., description="ユーザーからのセキュリティに関する質問")
answer: str = Field(..., description="質問への回答")
sources: List[str] = Field(..., description="回答の根拠となった URL リスト")
security_agent_instructions = """\
You are a cybersecurity expert assistant.
When given a question about the security of a specific service, you must:
1. Provide a clear, concise answer to the user's question.
2. List 2–3 external sources (URLs) that you actually retrieved via the WebSearchTool and that directly support your answer.
Format your output as JSON conforming to the SecurityAnswer schema.
Do not output any additional fields."""
security_agent = Agent(
name="SecurityQA",
instructions=security_agent_instructions,
tools=[WebSearchTool()],
output_type=SecurityAnswer,
# (必要に応じて model や temperature を指定)
)
if __name__ == "__main__":
# 例:サービス名を入れて質問
question = "AWS S3 のバケットポリシーでパブリックアクセスを完全に防ぐ方法は?"
result: SecurityAnswer = Runner.run_sync(security_agent, question)
print(result.json(indent=2, ensure_ascii=False))
例えば、AWS S3のバケットポリシーのセキュリティについて質問してみると、以下の様な結果が得られます。
アウトプット
{
"question": "AWS S3 のバケットポリシーでパブリックアクセスを完全に防ぐ方法は?",
"answer": "AWS S3 バケットのパブリックアクセスを完全に防ぐには、以下の手順を実施します:\n\n1. **パブリックアクセスブロック設定の有効化**:\n - **バケットレベル**:各バケットのパブリックアクセスブロック設定を有効にします。これにより、バケットポリシーやアクセスコントロールリスト (ACL) を通じてパブリックアクセスが付与されるのを防ぎます。\n - **アカウントレベル**:アカウント全体でパブリックアクセスブロック設定を有効にすると、現在および将来のすべてのバケットとアクセスポイントに対してパブリックアクセスがブロックされます。([docs.aws.amazon.com](https://docs.aws.amazon.com/ja_jp/AmazonS3/latest/userguide/access-control-block-public-access.html?utm_source=openai))\n\n2. **アクセスコントロールリスト (ACL) の無効化**:\n - ACL を無効にすることで、バケットやオブジェクトへのアクセス制御を IAM ポリシーやバケットポリシーに一元化し、より安全な管理が可能となります。2023 年 4 月以降、新規作成されるバケットではデフォルトで ACL が無効化されています。([aws.amazon.com](https://aws.amazon.com/jp/about-aws/whats-new/2022/12/amazon-s3-automatically-enable-block-public-access-disable-access-control-lists-buckets-april-2023/?utm_source=openai))\n\n3. **バケットポリシーの確認と設定**:\n - バケットポリシーを確認し、パブリックアクセスを許可する設定がないことを確認します。必要に応じて、特定のプリンシパルや条件に基づいてアクセス許可を設定します。([docs.aws.amazon.com](https://docs.aws.amazon.com/ja_jp/AmazonS3/latest/userguide/example-bucket-policies.html?utm_source=openai))\n\nこれらの手順を実施することで、AWS S3 バケットへのパブリックアクセスを効果的に防ぐことができます。",
"sources": [
"https://docs.aws.amazon.com/ja_jp/AmazonS3/latest/userguide/access-control-block-public-access.html",
"https://aws.amazon.com/jp/about-aws/whats-new/2022/12/amazon-s3-automatically-enable-block-public-access-disable-access-control-lists-buckets-april-2023/",
"https://docs.aws.amazon.com/ja_jp/AmazonS3/latest/userguide/example-bucket-policies.html"
]
}
OpenAI Agents SDKとは
openai-agents-python
は、大規模言語モデル(LLM)を「エージェント」として扱い、Python だけで高度なやりとりを行える軽量なライブラリです。検索などのツールを呼び出したり、別のエージェントに処理を渡したりといった仕組みを簡単に組み込めます。
OpenAI 以外を含む100以上のモデルに対応しており、動作の記録・可視化(トレーシング)機能も標準搭載。数十行のコードで試せる手軽さと、本番環境でも使える柔軟さをあわせ持った Python 向けの開発キットです。
エージェントを活用した典型的なコード例は、こちらのリンクにまとまっています。
ファイル名 | 説明 |
---|---|
deterministic.py |
タスクを小さなステップに分け、逐次処理する決定論的なフローの例 |
routing.py |
タスクを適切なエージェントにルーティングするハンドオフの例 |
agents_as_tools.py |
エージェントをツールとして使い、独立して動作させる方法の例 |
llm_as_a_judge.py |
LLMを判定者として活用し、フィードバックをもとに出力を改善する方法の例 |
parallelization.py |
エージェントを並列で実行し、最適な結果を選択する方法の例 |
input_guardrails.py |
入力のガードレールを設定し、不適切なリクエストを排除する方法の例 |
output_guardrails.py |
出力のガードレールを設定し、適切なレスポンスを保証する方法の例 |
Agent の詳細
以下は、基本的な使い方を簡潔にまとめたものです。
公式ドキュメントでも詳しく紹介されていますが、ここでは今回使用する主なモジュールに絞ってご紹介します。
1.基本設定
エージェントの基本的な設定は、名称と指示・モデル、エージェントが使用することができるツールとしての関数がある。
from agents import Agent, ModelSettings, function_tool
@function_tool
def get_weather(city: str) -> str:
return f"The weather in {city} is sunny"
agent = Agent(
name="Haiku agent",
instructions="Always respond in haiku form",
model="o3-mini",
tools=[get_weather],
)
2.Tools
データ取得、コード実行、外部API呼び出し、さらにはコンピュータ操作の自動化などができるツールのこと。
Agent SDKには、以下の4種類のツールが用意されている
- LLMモデルが動作するサーバ上で提供されているもので、OpenAIがあらかじめ用意されているHosted tools
- Pythonの任意の関数をツールとして利用できる Function tools
- Python関数以外の独自のロジックでツールを作成したい場合、手動で定義する Custom function tools
- エージェント同士が互いに呼び出しあう仕組みで、特定分野のエージェントをツールとして組み込むことを可能にする Agents as tools
3.Context
「コンテキスト」は、
- エージェントが実行中に使うデータや関数(外部APIを呼び出す、データベース接続を扱うなど)をひとまとめにしたもの
- エージェントが動作するとき、必ず1つの「コンテキスト」というオブジェクトが渡される
- Pythonオブジェクトであればなんでもコンテキストとして指定可能です(カスタムクラスやdataclassなど)。
依存性注入(Dependency Injection)を行うために使う。
→ エージェント自身が必要な外部のデータや処理を自分で生成するのではなく、外側から「コンテキスト」として渡してあげる、という仕組み。
イメージ
@dataclass
class UserContext:
uid: str
is_pro_user: bool
async def fetch_purchases() -> list[Purchase]:
return ...
agent = Agent[UserContext](
...,
)
4.Output types
デフォルトのstr
(文字列)形式ではなく、構造化されたデータを返してほしい場合は、output_type
というパラメータを使って出力タイプを指定できる。
Pydanticを使うことが一般的とされるがデータクラス、リスト、TypedDictなど、 Pydantic TypeAdapter でラップできるあらゆるタイプをサポートしている。
from pydantic import BaseModel
from agents import Agent
class CalendarEvent(BaseModel):
name: str
date: str
participants: list[str]
agent = Agent(
name="Calendar extractor",
instructions="Extract calendar events from text",
output_type=CalendarEvent,
)
5.Dynamic instructions
エージェントの動作時に状況に応じて毎回異なる指示を与えるための機能。
-
通常(静的):エージェント作成時に決めた固定の命令(指示)。
from agents import Agent agent = Agent( name="Simple agent", instructions="ユーザーの質問に答え、簡潔に対応してください。", )
-
動的:実行時の状況に応じて変化する命令を動的に生成
from dataclasses import dataclass from agents import Agent, RunContextWrapper, function_tool @dataclass class UserContext: uid: str name: str # ← ここにユーザーの名前が入っている想定 is_pro_user: bool def dynamic_instructions( context: RunContextWrapper[UserContext], agent: Agent[UserContext] ) -> str: return f"The user's name is {context.context.name}. Help them with their questions." agent = Agent[UserContext]( name="Triage agent", instructions=dynamic_instructions, )
6.エージェントの実行
提供されている3つのメソッドは以下の通り:
-
Runner.run()
:非同期実行-
実行例
# 非同期のWebアプリケーション内でエージェントを呼び出す場合 async def user_request(): agent = Agent(name="WebAssistant", instructions="質問に簡潔に回答してください。") result = await Runner.run(agent, "東京の天気は?") return result.final_output
-
-
Runner.run_sync()
:同期実行-
実行例
# 単純なCLIスクリプトでエージェントを利用する場合 def run_agent_sync(): agent = Agent(name="CLIAgent", instructions="ユーザーの質問に回答します。") result = Runner.run_sync(agent, "富士山の高さは?") print(result.final_output)
-
-
Runner.run_streamed()
:非同期+ストリーミング-
実行例
# チャットの応答をリアルタイムにストリームする場合 async def stream_agent_response(): agent = Agent(name="ChatAgent", instructions="質問に詳しく回答します。") result_stream = await Runner.run_streamed(agent, "AIとは何か?") async for event in result_stream.stream_events(): print(event.content, end='', flush=True) # リアルタイム表示
-
非同期処理はWebアプリなど、多数のユーザーが同時アクセスするサービスで、エージェントへのリクエストを並行処理したい場合などに使う。同期は非同期処理をする必要がない、または対応できない環境で使う。
想定問答の作成
ここでは、GitHub Copilotに関するセキュリティレビューを想定し、次の3つの質問をExcelシートにまとめました。
後続のデモ実行では、このシートを読み込んで自動回答を取得する流れを確認します。
想定シートのレイアウト
以下のように、質問ID、質問文、回答、参照情報(trail)を格納する列を用意します。
列名 (英語) | 意味・用途 |
---|---|
question_id | 質問の識別子(連番など) |
question | 質問文 |
answer | 自動応答で取得した回答を格納する列 |
trail | 回答の根拠となる参照URL一覧などを保存する列(JSON文字列やカンマ区切り想定) |
質問一覧
question_id | question |
---|---|
1 | 過去2年間に、ホームページ等で対外的に公表もしくは監督省庁へ報告するレベルのセキュリティインシデントがありましたか? |
-
出題意図
- インシデント履歴確認:社会的信用リスクを把握するため
デモ実行&結果
デモコード
以下では、先に設計したセキュリティ問答エージェントを用い、Excelシートに未回答の質問があれば自動でAgentに問い合わせて回答を埋めるデモを行います。本サンプルでは、pandasでExcelを読み書きし、Agent実行にはWebSearchToolを用いて外部検索結果を参照する流れを示します。
前提として:
-
check_sheet/security_check_example.xlsx
にはquestion
カラムとanswer
カラムがあり、未回答行のanswer
が空またはNaN であること。 - WebSearchToolが動作可能な環境(インターネット接続やAPIキー設定)が整っていること。
- Pydanticやagentsライブラリがインストールされていること。
import pandas as pd
import json
from pathlib import Path
from dataclasses import dataclass
from typing import List
import asyncio
from pydantic import BaseModel, Field
from agents import WebSearchTool, Agent, Runner, RunContextWrapper, function_tool
# ── (1) Context クラス ───────────────────────────────────────────
@dataclass
class ServiceContext:
service_name: str
# ── (2) 出力モデル ───────────────────────────────────────────────
class SecurityAnswer(BaseModel):
question: str = Field(..., description="ユーザーからのセキュリティに関する質問")
answer: str = Field(..., description="質問への回答")
sources: List[str] = Field(..., description="回答の根拠となった URL リスト")
# ── (3) 動的プロンプト関数 ────────────────────────────────────────
@function_tool
async def security_instructions(
run_context: RunContextWrapper[ServiceContext]
) -> str:
svc = run_context.context.service_name
return f"""\
You are a cybersecurity expert assistant specialized in **{svc}**.
When given a question about the security of {svc}, you must:
You are a cybersecurity expert assistant.
When given a question about the security of a specific service, you must:
1. Provide a clear, concise answer to the user's question.
2. List 2–3 external sources (URLs) that you actually retrieved via the WebSearchTool and that directly support your answer.
Format your output as JSON conforming to the SecurityAnswer schema.
Do not output any additional fields."""
security_agent = Agent[ServiceContext](
name="SecurityQA_WithContext",
instructions=security_instructions, # ← 動的関数をここに渡す
tools=[WebSearchTool()], # ← Context 用の関数はツールではなく instructions なので除外
output_type=SecurityAnswer,
)
def main():
df = pd.read_excel(Path("./check_sheet/security_check_example.xlsx"))
for idx, row in df.iterrows():
if pd.notna(row.get("answer")):
continue
svc_ctx = ServiceContext(service_name="Github Copilot")
question = row["question"]
# Runner.run_sync に context=svc_ctx を渡す
result: SecurityAnswer = Runner.run_sync(
security_agent,
question,
context=svc_ctx
)
result =result.final_output
# DataFrame へ書き込み
df.at[idx, "answer"] = result.answer
df.at[idx, "trail"] = json.dumps(result.sources, ensure_ascii=False)
df.to_excel("security_check_filled.xlsx", index=False)
print("回答済みシートを security_check_filled.xlsx に出力しました。")
if __name__ == "__main__":
main()
結果
以下は、上記コードを実際に実行した例として取得されたJSON形式の回答サンプルです。実行ごとに検索結果が異なるため、あくまで一例としてご覧ください。
また、APIはAgentが実行された際の入出力や回答に使用したtoolなど、一連の流れをDashboardから詳細に確認することができるのでご自分でAPIを使われる際は合わせてご活用ください。
{
"question": "過去2年間にホームページ等で対外的に公表もしくは監督省庁や認証機関等へ報告するレベルのセキュリティインシデントがありましたか。",
"answer": "GitHub Copilotに関して、過去2年間にホームページ等で対外的に公表された、または監督省庁や認証機関等へ報告されたセキュリティインシデントの具体的な事例は確認できませんでした。ただし、GitHub Copilotが生成するコードに脆弱性が含まれる可能性や、プライバシーに関する懸念が指摘されています。これらのリスクに対処するため、GitHubはセキュリティ対策を強化し、2024年6月にはSOC 2 タイプ 1レポートとISO/IEC 27001:2013認証を取得しています。",
"sources": [
"https://github.blog/jp/2024-06-14-github-copilot-compliance-soc-2-type-1-report-and-iso-iec-270012013-certification-scope/",
"https://snyk.io/jp/blog/github-copilot-xss-react/",
"https://levtech.jp/media/article/column/detail_340/"
]
}
AIの回答が必ずしも正しいわけではありませんが、回答に対する根拠となるソースを添えて回答してもらうことで調査の手間は大幅に削減できます。
まとめと今後の展望
いかがでしたか?
少々駆け足な内容になりましたが、今後同じような業務に携わる方の一助になれば幸いです。
今後は、生成AI特有の出力内容のブレやソースとして提示されるサイトURLのハルシネーション対策など、より実践的な改善に取り組んでいくつもりです。
Discussion