SARIFレポート

--sarif <path>は、--jsonが出すエンジンの生の結果の代わりに、またはそれと並べて、SARIF 2.1.0のログを書き出します。GitHub Code Scanning(やその他のSARIFを読み込むダッシュボード)が前提とする標準形式です。

$ surea11y scan ./dist/index.html --sarif results.sarif

スキャンを実行してresults.sarifを「Security」タブにアップロードする、そのまま貼り付けて使えるGitHub ActionsのワークフローはCI連携を参照してください。

--jsonとは別の形式にしている理由

--jsonの生の結果(出力スキーマを参照)はこのエンジン独自の契約で、API_STABILITY.mdに従ってバージョン管理され、安定しています。SARIFはそれとは別の、外部で定義された契約で、コードスキャンのダッシュボード向けに作られています。checksResults[]のエントリーとSARIFのresultは1対1に対応しないので、これは単なるシリアライズのし直しではなく、本当の意味での変換です。

SARIFのresultになるもの

SARIFのresultになるのはfail/cantTellの検出箇所だけです(HTMLレポートと同じく「違反だけ」という考え方です)。そのため、resultのないログが示すのは、実行されたチェックの中に不合格やレビューが必要なものがなかったということだけです。何がチェックされたかは示さず、適合の表明でもありません。それにはJSONの結果を保存してください。

notApplicableのチェックが常に空とは限りません。ルールは、判定する対象がなかった理由を説明する検出箇所を1つ付けることがあり、コントラストのルールは、背景を計算できるテキストが1つもなかったときにそうします。これらがresultになることはありません(利用側はすべてのresultをアラートとして扱いますが、「評価されなかった」はアラートではないからです)。ただし、捨てられもしません。runs[0].invocations[0].toolExecutionNotificationsにnoteレベルのエントリーとして記録され、それぞれassociatedRule.idで元のルールを示します。

"invocations": [
{
"executionSuccessful": true,
"toolExecutionNotifications": [
{
"level": "note",
"message": { "text": "No eligible text had computable contrast (eligible text nodes: 13). See the contrast computability rule for details." },
"associatedRule": { "id": "contrast-minimum" }
}
]
}
]

これにより、SARIFだけを使うパイプラインが、何も報告がないことを問題なしと読み違えずに済みます。コントラストのアラートがないのは、ページに問題がないからかもしれませんし、コントラストがそもそも計算できなかったからかもしれません。その2つを区別できるのはこの通知だけです。このブロックは伝えることがあるときにだけ出力されるので、報告することがない実行ではinvocationsキー自体がありません。合格したルールは通知を書き出しません。

完了しなかったルール(例外を投げたもの。検出箇所がなくerrorを持つcantTell)は何も判定していないので、これもresultにはなりません。同じリストのerrorレベルの通知"The rule <id> did not complete: <error>"になり、associatedRule.idでルールを示します。そのため、カバレッジの抜けが合格と読まれることはありません。

エンジンの判定結果SARIFのlevel意味
failerror決定論的な違反です。CIのゲート判定に使うのはこのケースです。
cantTellwarning人間によるレビューが必要です。表示はされますが、それだけでビルドを止めるべきではありません。
failに傾くcantTell(leanFailAs: 'error'指定時)error未判定ですが、ページの根拠が不適合を指しています(lean)。errorによるゲートで数えられます。

傾きのある未判定の検出結果は、どのlevelで報告されても、それをproperties.leanに持ちます:{ "toward": "fail", "why": "The heading skips a level after the one before it." }。傾きにあれば、誤りになりうる要因であるcaution("A layout table can also hold numbers.")と、これまでどれだけ正しかったかを示すmeasured({ "agreed": 47, "cases": 50, "corpus": "…", "coreVersion": "…" }。5件のケースで数えられた傾きにだけ付きますが、まだそうした傾きはありません)も持つので、アラートはHTMLレポートが傾きと並べて示すものを示します。signalsとi18nのキーはスキャン結果に残ります。

WCAGの達成基準がすべてスキャンの目標の外にあるルール(engineOptions.wcagLevel: 'AA'のもとでのAAAのルールや、WCAG 2.2のもとでのduplicate-id。後述のwcag-<SC>タグと同じく、スキャンがrulesResultsから外した達成基準です)も実行されており、そのfailとcantTellの検出結果はresultとして残ります。レベルはnoteで、目標に合わせたビルドを不合格にせずに目に入るようにしています。それぞれproperties.outsideTargetで理由を示します:{ "criteria": ["1.4.6"], "target": "WCAG 2.2 AA" }。

実行されたルールは(resultを出したかどうかにかかわらず)すべてruns[0].tool.driver.rulesに1度ずつ列挙され、defaultConfiguration.levelはルールのtypeから決まります。automatic(failを出せる)ならerror、manual(failにはならない)ならwarningです。保存された結果で文字列でないルールIDは、そのテキストとして書き出されます。ルールIDのないチェックはどのルールも示さないため、JUnitやEARLと同じく省かれます。

フィールドの対応

SARIFのフィールド元の値
results[].ruleIdchecksResults[i].ruleId
results[].message.textoccurrence.summary+occurrence.hint。要約のない検出箇所(カスタムルールのものなど)では、ルールのタイトル、またはそのIDを使います。GitHubは空のテキストを持つresultを拒否するためです。未判定の検出結果では、空行の後に手順が続きます。How to check:、番号付きの手順、そしてPasses when:とFails when:で、アラートがそれへの答え方を示すようにします。
results[].properties.howToCheck未判定の検出結果の手順で、occurrence.howToCheckが示すとおりの{ steps, passWhen, failWhen }です。バインディングの結果のように手順を持たない結果には、Nodeで追加されます。packs(スキャンが実行したパック)を渡すと、パックのルール自身の手順も追加されます。傾きと同じく、検出結果の同一性には含まれません。
results[].locations[].physicalLocation.artifactLocation.uriスキャン対象そのものです。後述の位置情報を参照してください。
results[].locations[].logicalLocations[].fullyQualifiedName要素の位置で、kind: "element"を伴います。occurrence.selectorが空でない文字列の場合に含まれます。シャドウツリーの外ではセレクターそのものです。シャドウツリー内の要素では、シャドウホストのセレクター、続いて要素自身のセレクターを >>> でつないだものです:html > body > main > x-card >>> button。JUnitのselector:行とHTMLレポートも同じテキストを示します。
results[].partialFingerprints["surea11y/violation/v2"]ベースライン/許可リスト(computeBaselineKey)で使うものと同じruleId + reasonCode + htmlの識別キーのSHA-256ダイジェスト(16進数64桁)です。位置ではなく内容に基づく、安定したフィンガープリントで、これを読む利用側のためのものです。同じ検出結果は、どのレポートでも同じダイジェストになります。1.11より前はsurea11y/violation/v1がキーそのものを保持していたため、v1を読んでいたツールでは、各検出結果が一度だけ新規として扱われます。理由コードが変わった検出結果は、別のダイジェストになります。1.14では、新たにコードを示すようになった17件のルールの検出結果と、原因ごとに分けられたコードの検出結果がこれに当たり、それらのCode Scanningのアラートは一度閉じられ、検出結果は新規として戻ります(ベースライン/許可リストを参照)。
results[].partialFingerprints.primaryLocationLineHashダイジェストの先頭16桁の16進数とカウントで、<16 hex digits>:<n>の形です(v2以前から変わっていません)。nは、ページ上で繰り返される検出結果(同じ壊れたコンポーネントが2つある場合など)を数えます。GitHub Code Scanningは、アップロード間のアラートをこのフィールドだけで照合します。DOMの検出結果には、GitHubのアップロードアクションがハッシュ化できる行がありません。そのため、これがなかったときはアラートが位置で照合され、ある検出結果を修正すると別の検出結果のアラートが閉じることがありました。core 1.8以前から移行すると、最初のアップロードで既存のアラートがいったん閉じられ、新しいアラートとして開き直されます。これは一度だけです。GitHubで行った却下(dismiss)は、もう一度行う必要があります。
results[].properties.severity/.confidence/.reasonCode/.html結果自身のseverity/confidenceと、上のフィンガープリントのキーになっているのと同じreasonCodeとhtmlの断片です。参考情報であり、SARIF自体のスキーマには含まれません。
tool.driver.rules[].properties.tagsaccessibility、automatic/manual、そしてmeta.normativeMappings内のWCAG達成基準のうち、スキャンのWCAGのレベルとバージョンが残すものごとに1つのwcag-<SC>タグです。それらによってスキャンがrulesResultsから外した達成基準にはタグが付きません。engineOptions.wcagLevel: 'AA'のもとでのAAAの達成基準や、WCAG 2.2でincludeRuleIds: ['duplicate-id']を実行したときの4.1.1です。スキャンが報告する達成基準は、レベルより上でもIDで指定された集約のように、タグを保ちます。engine.wcagLevelもengine.wcagVersionも記録していない結果では、すべての達成基準がタグを保ちます。ルール自体は残り、対応するすべての達成基準が外された場合は、そのresultもnoteとして残ります(前述を参照)。Understanding文書のエントリーにはタグを付けません。結果が持つEN 301 549の各条項(スキャンが要求した場合のみ。ほかの規格を参照)にはen301549-<条項>タグ(例:en301549-9.1.1.1)が付きます。条項番号はそれを含むどのバージョンでも同じなので、タグにバージョンは含めません。パックの規格やチェックリストも、それ自身のキーで同じようにタグ付けされます(チェックリスト項目のid、acme-imagesがそのままタグになります)。続いて、ルール自身のmeta.tags(カスタムルールのものも含む)が入ります。エンジンの管理用のタグと、wcag-<SC>タグがすでに示している達成基準のタグは除きます。
tool.driver.rules[].helpUriルールのヘルプリンクであるmeta.helpUrlが絶対http(s) URLならその値、そうでなければ対応する最初のWCAG達成基準のUnderstanding文書です。どちらもないルールでは含まれませんが、組み込みルールにそうしたものはありません。どの達成基準にも対応しないルールは、ルールカタログにある自身の節にリンクします。
tool.driver.versiontoolVersionオプションが指定されていればその値(エンジンを組み込んだツールは自身のバージョンを渡します)、なければ結果を生成したエンジンのリリースであるengine.versionです。"0.0.0"になるのは、1.10.0より前のリリースによる、この値を持たない結果か、engine.versionが文字列でない結果の場合だけです。
runs[0].automationDetails.idcategoryオプションの値で、末尾に/がなければ追加します(GitHubは最後のスラッシュまでをカテゴリーとして読むためです)。カテゴリーを指定しない場合は含まれません。複数のビューポート幅でのスキャンを参照してください。
runs[0].properties結果のengineにあるwcagVersion、wcagLevel(スキャンにレベルがあった場合。engineOptions.wcagLevel、または選択が示すレベル)、profile、optInRules、environment(engine.environmentのコピー)です。実行が使った適合性の目標を示すので、ダッシュボードはWCAG 2.1の実行と2.2の実行を、またAAの実行とすべてのレベルの実行を区別できます。その目標を超えてオプトインルールのタグを追加した場合は、それも示します。また、ページがレンダリングされた条件(layout、viewport、devicePixelRatio、colorScheme、forcedColors、fonts、フックのものを含むそのほかのすべてのフィールド。出力スキーマを参照)も示します。これらのフィールドより前のリリースの結果では省略されます。

コンパクトな結果(engineOptions.output.detail: 'findings'。出力スキーマを参照)は、それが表す完全な結果として読まれるので、同じように報告されます。standardsの一覧がないか壊れている結果でも、ルールが指定するすべての規格がタグ付けされます。

位置情報

DOMベースのスキャンには報告できる行/列がないので、physicalLocation.artifactLocation.uriはソースファイル上の位置ではなく、スキャン対象そのものになります。

  • ローカルファイルのスキャン:現在の作業ディレクトリ内のファイルは、そこからの相対パス(区切りはスラッシュ)になり、各セグメントはパーセントエンコードされます(build%20out/index.html)。これがリポジトリ内の実在するファイルと一致すれば、GitHub Code Scanningは検出結果をインラインの注釈として表示できます。作業ディレクトリの外にあるファイルは、../でリポジトリの外へ出る相対パスではなく、絶対file: URLになります。そのような相対パスは、受け取る側が解決できないためです。
  • URLのスキャン:スキャンしたURLそのものです。GitHub Code Scanningは検出結果を一覧には表示しますが、リポジトリ内のファイルではないURLにインラインの注釈を付けることはできません。これはSARIF/Code Scanningが検出結果とソースを結び付ける仕組みによるもので、surea11yの制限ではありません。インラインの注釈が必要なら、公開中のURLではなく、レンダリング済みのHTMLファイル(ビルド成果物など)をスキャンしてください。URLはURLパーサーがシリアライズするとおりに書き出されるため、URIに入れられない文字はパーセントエンコードされます(https://example.test/a%20b)。
  • その他の対象:urlがURLでない結果、たとえばページをスキャンしたときのパス(runDomRulesInPage('dist/my page.html', …))は、相対パス(区切りはスラッシュ)になり、各セグメントはパーセントエンコードされます(dist/my%20page.html)。Windowsのパスは絶対file: URLになります。urlがない結果や、文字列でない結果の位置はabout:blankです。

要素の位置(そのセレクター。シャドウツリー内にある場合は、シャドウホストのセレクターと >>> を前に付けたもの)はlogicalLocations[].fullyQualifiedNameとしても記録されるので、論理的な位置を読む利用側なら、使える物理的な位置がなくても「どの要素か」という情報を得られます。

フレーム横断の結果

runa11yCoreAcrossFrames、またはバインディングの.frames(true)によるスキャンの結果(出力スキーマを参照)は1つの実行になり、すべてのフレームの検出結果がその中のresultになります。フレーム内の検出結果の位置はそのフレームのドキュメントで(artifactLocation.uriはフレームのURL)、properties.frameにフレームのパスを持ちます。

"properties": {
"severity": "serious",
"confidence": "high",
"reasonCode": "DEFAULT",
"html": "<img src=\"card.png\">",
"frame": ["#checkout"]
}

パスは検出結果の識別(partialFingerprints)にも含まれるため、ページとフレーム、あるいは2つのフレームにある同じ欠陥は、2つのアラートになります。応答しなかったフレーム(応答側がない、またはタイムアウトした)は、その中の何も検査されていないため、warningレベルの通知"The frame #ads (https://ads.example/) was not scanned: <error>"になります。

複数のビューポート幅でのスキャン

レイアウトに依存するルールは、あるビューポート幅ではfailになり、別の幅ではpassになることがあります(既知の制限事項を参照)。そのため、2つの幅でスキャンしたページは、1つの分析ではなく2つの分析です。それぞれに独自のカテゴリーを指定してください。

const { renderSarifReport } = require('@surea11y/core/sarif');
 
for (const width of [390, 1280]) {
// ...scan the page at this width into `result`...
fs.writeFileSync(`a11y-${width}.sarif`, renderSarifReport(result, { category: `a11y-${width}` }));
}

そのうえで、各ファイルを別々にアップロードします。GitHub Code Scanningは、カテゴリーごとに独立した分析として扱います。1つのカテゴリー内では、後のアップロードが前のものを置き換え、そこに含まれないアラートは閉じられます。そのため、2つの幅を同じカテゴリーでアップロードすると、幅に固有のアラートが実行のたびに開いたり閉じたりします。また、GitHubは、同じツールとカテゴリーの実行を2つ含む1つのSARIFファイルを拒否します。github/codeql-action/upload-sarifにはcategory入力があり、ファイルにカテゴリーがない場合は同じ役割を果たします。

検出結果の識別にはビューポートは含まれず、含める必要もありません。同じ要素の同じ欠陥は、どの幅でも同じ検出結果であり、分析を分けておくのはカテゴリーの役割です。runs[0].properties.environmentには、各実行が使ったビューポートが記録されます。

オプション

@surea11y/core/sarifのrenderSarifReport(result, options)は、ログをJSONのテキストとして返します。次のオプションを取ります。

オプションデフォルト効果
toolVersionengine.versiontool.driver.versionです。エンジンを組み込んだツールのためのものです。
informationUri'https://github.com/SureA11y/core'tool.driver.informationUri、つまりツールのホームページです。
baselineEntriesなし保存したままのベースラインファイル(それをJSON.parseしたもの)、またはそのentriesの一覧です。ファイル形式を参照してください。そこに記録されたfailの検出箇所は、--baselineと同じく省かれます(後述)。1.13が書いたエントリーのうち、1.14で変わった理由コードのものも、新しいコードの検出結果に一致します。
categoryなしruns[0].automationDetails.idです。複数のビューポート幅でのスキャンを参照してください。
leanFailAs'warning''error'にすると、傾きが不適合を指すcantTellの検出結果をerrorで報告するため、errorでゲートするビルドは不適合の可能性が高いものでも失敗します。大文字と小文字は区別しません。ほかの値を指定するとTypeErrorが投げられます。ベストプラクティスのルールも対象です。そうしたルールはどのWCAG達成基準にも対応せず、failになることはありませんが、示す事実は無条件で不適合に傾くため、見出しレベルの飛び(heading-order)はerrorになります。対応するすべての達成基準がスキャンの目標の外にあるルールの検出結果はnoteのままです。不適合に傾き、baselineEntriesにlean: 'fail'と示されたエントリーがある検出結果はwarningのままです。
leanFailWcagOnlyfalseleanFailAs: 'error'と組み合わせてtrueにすると、スキャンの目標内のWCAG達成基準に対応するルールの検出結果だけを引き上げ、ベストプラクティスのルールの傾きはwarningのままにします。trueとfalse以外の値を指定するとTypeErrorが投げられます。@surea11y/coreのleaningFailures(result, { wcagOnly })は、この2つのオプションが引き上げる検出結果を、ベースラインを適用する前の状態で列挙します。
packsなしスキャンが実行したパック(engineOptions.packs)です。Nodeでは、パックのルールの未判定の検出結果が、そのルール自身のmeta.reviewの手順(properties.howToCheck)を持ちます。

--baselineとの併用

一般的なSARIFの利用側には、「既知なのでこれでは止めない」という概念がありません。SARIFの出力でベースラインを忠実に反映する唯一の方法は、既知のfailの検出箇所をwarningに格下げするのではなく、完全に省くことです。

$ surea11y scan ./dist/index.html --baseline baseline.json --sarif results.sarif

cantTellの検出箇所がベースラインで除外されることはありません。buildBaselineEntries(result, { leanFail: true })で書いたベースラインは、不適合に傾く検出結果もlean: 'fail'と示したエントリーとして記録します(ベースライン/許可リストを参照)。leanFailAs: 'error'のもとでは、そうした検出結果はerrorに引き上げられずwarningのままなので、チームはすでにある不適合の可能性が高いものを受け入れつつ、新しいものでゲートできます。--write-baselineが記録するのはfailの検出箇所だけです。

--html/--jsonとの併用

--sarif、--html、--jsonは互いに独立した出力フラグです。1回の実行でどう組み合わせても構いません。どれも同じ1回のスキャンから、それぞれのレポートを書き出し/出力します。