1 分

エージェントのpull requestを止めるべきゲートはどれか

安全でないコードを止める7つの測定可能なagent pull request gateを解説します。テスト、CodeQL、依存関係、secret、認可、migration、rollbackが対象です。

エージェントのpull requestを止めるべきゲートはどれか

エージェントは、きれいなdiffと説得力のある説明、短時間のレビューを通るコードを作りながら、アプリを攻撃可能または復旧不能な状態にできます。mergeの判断は、エージェントの自信やpatchの小ささではなく、repositoryが測定できる証拠に基づくべきです。

私は7つのblocking gateを使います。テスト、CodeQL、依存関係レビュー、secret scanning、認可チェック、migration rehearsal、rollback verificationです。それぞれ異なる障害を調べます。テストがgreenでも新しいpackageの安全性は証明できません。static analysisがcleanでも、migrationが最も忙しいtableをlockするかは分かりません。

これらのgateは、人間とエージェントの変更に同じように適用します。エージェントはミスの量、速度、形を変えますが、弱い別経路を用意する理由にはなりません。他のpull requestと同じ証拠を出せない変更はmergeできません。

ゲートが出すのは助言ではなく証拠

merge gateは、protected branchに入る正確なcommitについて、再現可能なpassまたはfailを返す必要があります。「この依存関係を確認してください」というcommentは助言です。package、version、advisory、severity thresholdを示すrequired checkは証拠です。

この区別は重要です。多くのsecurity機能は、判断の後に報告するだけだからです。scannerがalert、email、issueを作ってもmerge buttonは使えることがあります。チームはscanningが有効だと言いますが、変更を止められません。各gateで次の4点を確認します。

  • pull requestの現在のhead commitで実行する。
  • branch protectionがその名前のresultを必須にする。
  • skip、timeout、crashしたjobをpassと扱わない。
  • 判断を再現できる詳細をresultに残す。

policyはrepository内に置きます。administratorしか知らない設定の集合より、小さなmanifestのほうがレビューしやすくなります。

merge_gates:
  tests: required
  codeql: required
  dependency_review: required
  secret_scan: required
  authorization: required
  migration_rehearsal: required_when_changed
  rollback_verification: required

required_when_changedは抜け道ではありません。gateは関連fileを検出し、rehearsalを行うか、明示的なnot applicableを記録します。path filterによってrequired checkが永久にpendingになってはいけません。エージェント自身に危険な変更の免除を決めさせてもいけません。

workflow permissionは各jobに必要な最小限にします。pull requestのコードは、branchが自社organizationにあっても信頼できないinputです。検査するコードにwrite tokenやproduction secretを渡すgateは、検出対象より大きな問題を作りかねません。

checkのlogicだけでなくidentityも守ります。branch ruleは通常status nameを要求するため、同じ名前を報告できる2つのworkflowがあると弱いjobでruleを満たせます。policy jobに固有名を付け、fileを変更できる人を制限し、owner reviewを要求します。merge queueが新しいmerge commitを作るなら、そのcommitでgateを再実行するかresultをqueued revisionに結び付けます。昨日のheadの証拠は今日のmergeには使えません。

gate configurationもsensitive codeとして扱います。thresholdの変更、pathの削除、query packの格下げ、exceptionの追加は、以後のgreen resultの意味を変えます。policy diffを目立たせ、該当controlを理解するmaintainerのreviewを求めます。エージェントは変更を提案できますが、自分を判定する仕組みを編集したために通りやすくしてはいけません。

テストは観測できるregressionを止める

test gateは、supported runtimeとdatabase versionで定義済みのbehaviorを壊す変更を止めます。locked dependency、generated file、feature flag、schema stateを含め、merge commitと同じbuild inputを使います。

エージェントは最も近いassertionを満たすのが得意です。1つのtestをgreenにするfallbackを追加し、error handling、pagination、concurrency、隣のAPI contractを壊すことがあります。behaviorを変えるpull requestにはtestの追加または変更を求めますが、追加行数で品質を測りません。implementationを削除またはrevertしたとき、そのtestがfailするか確認します。

有効なtest gateには別名のlayerがあります。

  • local logicとboundary caseのunit test。
  • database、queue、cache、external service contractのintegration test。
  • public requestとresponse shapeのcontract test。
  • built artifactに対する小さなsmoke test。

flaky testはgreenになるまでretryせず、原因を直します。1回のretryでdiagnostic evidenceを集めることはできますが、final statusには最初のfailureを残します。そうしなければ、ときどき動くことしか証明されていないコードをmergeできます。

smoke testはartifactを起動し、health endpointと意味のあるwrite pathを実行し、正常に停止します。packaged applicationを起動せずsourceだけをtestすると、missing file、悪いenvironment default、壊れたmigration、startup panicを見逃します。web serviceの結果は次のようにできます。

{"commit":"abc123","build":"pass","startup_ms":842,"health":200,"write_read_cycle":"pass"}

一律のcoverage percentageを主gateにしません。coverageは未テストの変更を示せますが、弱いassertionでも高い数字になります。required suiteとchanged behaviorで止め、coverageの変化はreview evidenceに使います。

判定対象のimplementationからtestを守ります。pull requestがruleとassertionを同時に書き換えて新しい結果を受け入れると、suiteがpassしたままcontractが変わります。reviewerはchanged testをpublic API、issue、acceptance ruleと比較します。parser、validator、billing、access checkにはmutation testingや意図的に誤ったinputを加えます。もっともらしい誤実装をsuiteが拒否できるかが基準です。

gateがfailしたらtest artifactを残します。random testのfailing seed、正確なdatabase image、secretを除いたservice log、再現commandを保存します。再現情報のないstatusは次のagentやengineerを推測に戻します。repositoryのdata ruleに従って保持期間を制限し、再現を楽にするためだけにproduction snapshotをuploadしてはいけません。

CodeQLは既知の脆弱性経路を止める

CodeQL gateは、CodeQLが実際に解析したlanguageとgenerated artifactにおける、新しいhigh confidence findingを止めます。clean resultをアプリ全体の安全証明として扱ってはいけません。

GitHubはCodeQLを、コードをquery可能なdatabaseにcompileし、その上でqueryを実行する仕組みと説明しています。疑わしいtextだけでなくdata flowを追える点が有用です。一方、language support、build success、query selection、解析時のコードに制約されます。database buildがserviceを静かに除外したら、green resultがカバーする範囲はreviewerの想定より狭くなります。

languageとquery policyを明示したworkflowを使います。

name: codeql
on: [pull_request]
permissions:
  contents: read
  security-events: write
jobs:
  analyze:
    strategy:
      matrix:
        language: [javascript-typescript, go]
    steps:
      - uses: actions/checkout@v4
      - uses: github/codeql-action/init@v3
        with:
          languages: ${{ matrix.language }}
          queries: security-extended
      - uses: github/codeql-action/autobuild@v3
      - uses: github/codeql-action/analyze@v3

実際のrepositoryではthird party actionをreview済みcommit digestにpinします。tagは例を読みやすくしますが、mutable tagはrequired security jobのtrust boundaryを広げます。

最初のalertが出る前にblock条件を決めます。通常は合意したseverityとprecision thresholdで新しいfindingをblockし、既存のdebtはbaselineに表示します。初日に過去分をすべてblockすると大量dismissalを招きます。永久に無視すればblind spotになります。baselineにはownerと期限を付けます。

service、language、build commandが変わったらanalyzed file setを確認します。新しいmobile client、generated resolver、別backendには別のanalyzerやbuild stepが必要かもしれません。reviewerが解析対象を説明できないなら「CodeQL passed」の意味は限定的です。

analysis failureとclean analysisを分けます。autobuildがpackageをcompileできないなら、jobはinfrastructureまたはconfiguration failureを返し、zero findingにはしません。database creation logと言語別のanalyzed source countを保存します。base branchと比較して、理由のない大幅減を止めます。build editが脆弱なmoduleを除外し、少ないコードしか見なかったためcheckが速くgreenになる失敗を検出できます。

dismissalは掃除ではなくpolicy changeとしてreviewします。false positiveにはcode pathとqueryに結び付いた説明が必要です。suppression commentは範囲を狭くし、ownerを持ち、diffに表示します。generated file全体の除外が妥当でも、手作業で管理するtemplateやgenerator inputがないことを先に確認します。

依存関係レビューはinstall前に危険を止める

dependency diffが明示policyに違反するpackageやversionを追加したら、dependency reviewはpull requestを止めます。policyにはknown advisory severity、denied license、予期しないpackage source、ownerのないdirect dependencyを含められます。

このgateはrepository vulnerability alertとは異なります。alertはbranchに脆弱なdependencyがあると伝えます。dependency reviewは、このpull requestがgraphを悪化させるかを問います。GitHubの機能はmanifestとlockfileを比較するため、判断が変更に結び付きます。

lockfile consistencyを必須にします。エージェントがpackage.jsonを変えてlockfileを変えない場合や、対応するmanifest changeなしにlockfileを変えた場合はfailです。CIでfresh versionをresolveするinstall commandは非決定的で、reviewerが見たものと違うgraphをtestします。

小さなpolicyは次のようになります。

dependency_policy:
  fail_on_severity: high
  deny_licenses:
    - AGPL-3.0
  allow_sources:
    - registry.npmjs.org
    - proxy.golang.org
  require_owner_for_direct_additions: true

正確なlicense listは法務とproductの判断であり、そのままcopyする値ではありません。repositoryが宣言し、checkがtriggerしたpackageを表示することが重要です。

名前が提案されたlibraryに似ているだけでpackageをauto approveしません。エージェントは存在しないpackage名、放棄されたfork、小さなhelperのための巨大clientを選ぶことがあります。outputには新しいdirectとtransitive package、registry、resolved version、license、advisory statusを出します。reviewerは既存コードや小さなdependencyで足りるか判断できます。

dependency serviceがdiffを作れなければfail closedにします。advisory feedの停止はmergeを保留する理由にはなりますが、不明をgreenにする理由ではありません。緊急時はnamed maintainerによる記録済みoverrideを認め、理由をcommitに残します。

approved registryだけにnetwork accessできるisolated jobでinstall behaviorを検査します。lifecycle scriptとbuild pluginはinstall中にコードを実行するため、appがimportしなくても危険です。新しいinstall script、native binary、未知のregistryを記録します。publishing credential、cloud token、trusted buildと共有するwritable cacheを渡してはいけません。

vendored codeとcontainer imageも同じ判断対象です。通常のmanifest reviewが見逃しても、image digest、base image name、Git submodule、checked-in archiveを比較します。release inputにはimmutable digestを要求します。latestのようなtagはdiffなしでbytesが変わるため、後でpull requestを再現できません。

Secret scanningはdiffと履歴を検査する

rollbackの証拠と一緒に作る
chatで作成し、deploymentとsnapshotで以前のapplication stateへ戻れると証明します。

pull requestがcredential patternや検証済みlive secretを追加したら、test fixture、deleted file、generated bundle、以前のcommitにあってもblockします。

push protectionとpull request scanningは関連しますが役割が違います。push protectionはsecretがremoteへ届く前に止めます。pull request gateは到着済みのcontentを調べ、前段で漏れたcontributorやtoken typeをカバーします。branch protectionでrequiredになっていないalertはmergeを止めません。

final filesystemだけでなくbase branchからのcommit range全体をscanします。エージェントが1つのcommitでtokenを追加し、次で削除してもGit historyに残り、logやcacheへ届いた可能性があります。exposedとして扱い、credentialをrevokeまたはrotateし、提案履歴から削除してcheckを再実行します。

authenticateできないsynthetic fixtureを使います。fixtureはtestであることを明記し、real cloud keyの形をコピーせずlocal test patternに一致させます。広いallowlistは、事故や攻撃者がignored directoryを使うため危険です。exceptionは正確でreview済みにし、detector configurationの近くに置きます。

pattern detectorにentropy checkを組み合わせ、providerが安全に対応する場合だけcredential verificationを使います。pattern matchingはnetworkとdisclosureのriskが低い一方、custom tokenを見逃します。verificationは不確実性を減らしますが、候補secretの一部を別serviceへ送ります。pull requestが指定したuntrusted endpointには接続しません。どのdetectorがverifyし、どれがlocalで、何がCI外へ出るか記録します。

すべてのrandom stringをcredentialと扱わず、一般的なencodingとgenerated outputをscanします。Base64、URL encoding、minified bundleはsource stepで平文だったsecretを隠せます。tested fixtureでblocking thresholdを決め、detector updateはpolicy changeとしてreviewします。noisy scannerはdismissalを習慣にし、silent scannerは誤った安心を作ります。

gate outputは値を隠し、場所だけを示します。

{"result":"fail","detector":"generic-api-token","commit":"abc123","path":"config/dev.env","line":7,"fingerprint":"sha256:8f2c..."}

full matchをCI logやpull request commentへ出してはいけません。scanner出力後のmaskingでは遅く、log system、notification、job artifactがcopy済みかもしれません。

secret scanningはrepository permission reviewの代わりにはなりません。workflowはdiffにsecretを置かずにproduction credentialを読めますし、エージェントはdeployment jobを変えて持ち出せます。pull request jobからsecretを外し、workflow permissionを制限し、CI definition changeにhuman reviewを要求します。

認可チェックは許されない操作を拒否し続ける

authorization gateは、protected operationがservice boundaryで誤ったactorを拒否し、意図したactorを許可することを証明します。login testだけではauthorizationを確認できません。

チームはauthentication、authorization、UI visibilityを混同しがちです。authenticationはrequest送信者を特定します。authorizationは、そのidentityがそのobjectにactionできるかを決めます。Reactでadmin buttonを隠してもGo APIの判断は変わりません。backendがrequestを受け入れればアプリは無防備です。

変更されたendpointとbusiness actionに、ownershipとtenant boundaryを含むpermission matrixを作ります。

read_private_project:
  anonymous: deny
  member: deny
  other_tenant: deny
  owner: allow
  admin: allow
update_project:
  anonymous: deny
  other_tenant: deny
  owner: allow

matrixからtestを生成するか、service languageでtable-driven caseを実装します。各deny caseはrealistic identifierでreal handlerを呼びます。authorization middlewareを置き換えるmockはroute動作を証明しても、検査対象controlをskipします。

roleだけでなくobject-level accessをtestします。2人のuserが同じmember roleでも別organizationに属する場合があります。resource identifierとtenant identifierを別々に変え、insecure direct object referenceを見つけます。通常のREST handler checkを回避しやすいbulk endpoint、export、background job、GraphQL resolverも対象です。

新しいprotected routeにpolicy mappingがなければgateをfailさせます。missing authorizationがreviewerの勘ではなく測定可能なdefectになります。server defaultはdenyにします。既知の悪いcaseだけを拒否する分散コードより、explicit allow ruleのほうがauditしやすくなります。

エージェントは近くのhandlerをreuseし、happy pathを残してownership checkを落とすことがあります。matrixはこの漏れを表示し、roleやtenant ruleが変わってもstable contractになります。

input normalization後のauthorizationも実行します。大文字小文字、別のidentifier形式、重複query parameter、nested object referenceによって同じrequestが違うcode pathへ入ります。direct endpointと同じoperationに届くbatchまたはimport routeをtestします。background workerが最終writeを行うならactorとtenant contextをjobへ伝え、全権を持つtrusted userとして扱いません。

expected decisionと、それを生んだpolicy ruleを記録しますが、private object dataは出しません。有用なfailureはactor class、action、object class、expected statusを示し、access tokenやfull recordをdumpしません。reviewerはbroken fixtureと本当のprivilege changeを区別できます。

Migration rehearsalはlockと可逆性を測る

既知の正常stateを保つ
Koder.aiのsnapshotはrollback verificationに具体的なrecovery pointを与えます。

migration gateは、提案されたschema changeをproduction相当のcopyへ適用し、compatibility probeを実行し、merge前にduration、lock、rollback behaviorを記録します。空のtest databaseで成功しても、ほとんど証明になりません。

table size、index、constraint、data skewが似たsanitized snapshotまたはgenerated datasetを使います。正確なproduction dataをpull request infrastructureへ持ち込みません。個人情報や機密recordをcopyせず、運用負荷を再現します。

実際のdeployment sequenceをrehearseします。migration中にold app instanceが動くなら、old codeをnew schemaで、new codeをtransitional schemaでtestします。nullable columnの追加は通常compatibleです。1 stepでcolumnをrenameすると、trafficを処理中のold instanceを壊します。

machine-readableな証拠を残します。

{"migration":"20260727_add_project_state","apply_ms":18420,"max_lock_ms":310,"old_app_probe":"pass","new_app_probe":"pass","down":"pass"}

databaseとtable classごとにthresholdを決めます。300 millisecondのlockが問題ないtableもあれば、停止を招くtableもあります。reviewerはselected limit、measured result、rehearsal database versionを一緒に確認します。

PostgreSQLでは、table rewrite、constraint validation、writeをblockするindex buildを調べます。expand and contractを使います。新しいshapeを追加し、両方を扱うコードをdeployし、controlled batchでbackfillし、readを切り替え、後の変更でold shapeを削除します。追加のpull requestはoutage中の即興対応より安価です。

すべてのmigrationに安全なdownがあるわけではありません。column dropはdataを失い、transformationの逆変換は曖昧な場合があります。その場合はtested forward recoveryとbackup restore pointを要求します。down fileがあるだけでirreversible migrationをrollback safeと呼ぶのは不正確です。

retryとpartial failureをtestします。index作成後、migration完了記録前にdeployが止まる場合があります。次回実行はstateを壊したり永続的にfailしたりしてはいけません。controlled boundaryでrehearsalを止め、再実行し、schemaとledgerの一致を確認します。長いbackfillはrecorded cursorから再開し、同じbatchを2回実行してもduplicateやdeleteを起こさないようにします。

経過時間だけでなくdiskとreplicationの影響も見ます。table rewriteはtemporary spaceを使い、write ahead logを増やし、main operation終了後もreplicaを遅らせます。完全なproduction予測は不要ですが、rehearsal datasetで値を記録しdatabase ownerのlimitと比較します。そうしなければlocalで速いmigrationがproduction volumeを使い切ることがあります。

Rollback verificationは復旧経路を実行する

deployしたrollbackを試す
applicationをdeployしてsnapshotを取り、代表的なstateでKoder.aiのrollbackを検証します。

rollback gateはcandidateをisolated environmentへdeployし、representative stateを作り、supported rollback methodを実行し、previous versionが正しくreadとwriteできることを証明します。書かれたrollback planはverificationではありません。

application rollbackとdata rollbackを分けます。trafficをprevious binaryへ戻すのは数秒でも、destructive schemaやdata transformationは戻せない場合があります。gateは両方を報告します。old appがnew schemaで動かないならcandidateをnonreversibleとし、staged deployment planを要求します。

実用的な手順は次の通りです。

  1. 現在のmain commitをdeployし、representative recordをseedする。
  2. pull request artifactへupgradeし、changed pathを実行する。
  3. upgraded stateで新しいrecordを作る。
  4. documented controlでprevious artifactまたはsnapshotをrestoreする。
  5. read、write、queue、background jobのprobeを行う。

checkはartifact identifier、snapshot identifier、timestamp、probe resultを保持します。credentialやcopyしたcustomer dataは残しません。recovery timeは自分たちのoperating targetの証拠であり、普遍的な約束ではありません。

snapshotは、applicationに必要なすべてを含むと証明して初めて役立ちます。file、object storage、queue state、schema change、external side effectはserver snapshotの外にあるかもしれません。resultに境界を書きます。送信済みのpayment、email、webhookはdatabase restoreで取り消せません。

rollback probeをhealth checkより強くします。upgrade前と後に作ったrecordを読み、compatibilityがあれば両方をupdateし、各app versionが作ったqueued jobを処理します。status codeだけでなくuser-visible outputを比較します。new fieldを落としたりenumを誤読したりしながら200を返すserverは復旧していません。

on-call担当者が使うcontrolをtestします。production rollbackでartifact選択、snapshot restore、traffic切り替えが必要なら、isolated rehearsalも同じinterfaceとpermission pathを使います。1人のengineerのlaptopにあるprivate scriptはoperational controlではありません。designated operator roleがpermanent admin accessなしで復旧できることを証明します。

Koder.aiはsource export、deploymentとhosting、snapshot、rollbackに対応するため、そこで作ったapplicationは復旧をpull requestの文章で済ませず、具体的なartifactをgateで使えます。どのplatformでも同じです。recovery controlを実行し、restored applicationをprobeします。

1つのrequired checkで7つを集約する

最終merge判断では、正確なhead commitについて7つのresultを検証するstable policy checkを1つ必須にします。job nameは変わり、matrix jobは増え、optional pathは作業をskipします。小さなaggregatorがbranch protectionとpolicyのずれを防ぎます。

各gateはcommit、policy version、outcome、evidence locationを含むsignedまたはplatform-attested resultを出します。aggregatorはmissing、stale、neutral、cancelled resultをrejectします。報告しなかったjobからsuccessを推測してはいけません。

{
  "commit": "abc123",
  "policy": "merge-gates-v3",
  "results": {
    "tests": "pass",
    "codeql": "pass",
    "dependency_review": "pass",
    "secret_scan": "pass",
    "authorization": "pass",
    "migration_rehearsal": "not_applicable",
    "rollback_verification": "pass"
  },
  "decision": "allow"
}

緊急時のoverride pathは狭くします。named maintainer、2人目のapprover、written reason、expiry、follow-up issueを必須にします。エージェントが自分のexceptionを依頼または承認してはいけません。通常のgate resultと同じ場所にoverrideを報告し、incident後も見えるようにします。

これらのcheckは一部のpull requestに数分、migration changeにはさらに時間を加えます。具体的な証拠を得られるなら妥当です。cheap gateを先に実行し、superseded commitをcancelし、trusted build inputをcacheし、full environment rehearsalは関連pathだけに使います。dashboardを早くgreenにするためgateを弱めてはいけません。

まず現在のbehaviorを観測可能にします。1つのpolicy fileに7つの名前を書き、各名前をrequired resultにつなぎ、skipped workに説明を求めます。そのrecordを出せない最初のagent pull requestは、productionより先にdelivery systemの穴を見つけたことになります。

よくある質問

Agent pull requestには人間より厳しいruleが必要ですか?

両方に同じblocking evidenceを使います。エージェントの速度から自動checkを増やすのは妥当ですが、人間のdiffも同じsecret、認可bug、危険なmigrationを入れられます。

人間のreviewerは失敗したmerge gateをoverrideできますか?

可能ですが、2人の責任者、理由、期限を持つ狭い記録済みexceptionだけにします。変更を作ったエージェントが自分のoverrideを承認してはいけません。

CodeQLがpassすればpull requestは安全ですか?

いいえ。選択したqueryが、正常に解析されたcode内でblocking resultを見つけなかったという意味です。dependency、runtime authorization、secret、configuration、recoveryには別の証拠が必要です。

Required scannerを利用できない場合はどうしますか?

checkをfail closedにするか、mergeをblockしたままにします。緊急時はunknown resultをpassに変えず、documented override pathを使います。

Secret scanningは削除済みcommitも調べますか?

pull requestのcommit range全体を調べます。追加後に削除したsecretもhistoryに残るため、cleaned changeを受け入れる前にrotateします。

Pull requestでauthorizationをどうtestしますか?

identity、role、tenant、object、actionのmatrixでreal service handlerを呼びます。loginやhidden UIはserver authorizationを証明しないため、deny caseとownership changeも含めます。

すべてのdatabase migrationにdown scriptが必要ですか?

いいえ。正直に逆転できないdestructive changeもあります。tested forward recoveryまたはbackup restore procedureを要求し、nonreversibleと明記します。

Rollback planningとverificationの違いは何ですか?

planningは意図したrecovery stepを記述します。verificationはcandidate artifactとrepresentative stateで実行し、previous versionが正しくreadとwriteできるか記録します。

7つのgateで全pull requestが遅くならないようにするには?

cheap checkを先に実行し、superseded commitをcancelし、trusted inputをcacheし、関連changeだけmigration rehearsalします。各gateは明示的なpassnot applicableを返します。

Branch protectionはどのresultを必須にすべきですか?

正確なhead commitの7 gateを集約するstable policy resultを必須にします。missing、stale、skipped、cancelled、neutral resultはrejectします。

Related posts