@surea11y/core 1.2.0 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +37 -7
- package/LICENSE +373 -21
- package/README.md +67 -1
- package/bin/core.js +144 -19
- package/docs/API_STABILITY.md +1 -1
- package/docs/BINDING_AUTHORS_GUIDE.md +9 -9
- package/docs/CI_INTEGRATIONS.md +103 -0
- package/docs/CLI.md +54 -1
- package/docs/ENGINE_OPTIONS.md +2 -0
- package/docs/INTEGRATION.md +18 -0
- package/docs/OUTPUT_SCHEMA.md +1 -1
- package/docs/RULE_CATALOG.md +1 -1
- package/docs/SARIF.md +59 -0
- package/package.json +15 -4
- package/src/baseline.js +0 -0
- package/src/catalogs/composites.wcag.js +414 -450
- package/src/checks/automatic/area-alt-present.js +59 -25
- package/src/checks/automatic/aria-allowed-attr.js +193 -33
- package/src/checks/automatic/aria-allowed-role.js +21 -7
- package/src/checks/automatic/aria-braille-equivalent.js +32 -10
- package/src/checks/automatic/aria-conditional-attr.js +24 -7
- package/src/checks/automatic/aria-deprecated-role.js +22 -8
- package/src/checks/automatic/aria-hidden-body.js +42 -19
- package/src/checks/automatic/aria-hidden-focus.js +408 -53
- package/src/checks/automatic/aria-prohibited-attr.js +296 -22
- package/src/checks/automatic/aria-prohibited-children.js +55 -16
- package/src/checks/automatic/aria-required-attr.js +23 -8
- package/src/checks/automatic/aria-required-children.js +37 -14
- package/src/checks/automatic/aria-required-parent.js +48 -14
- package/src/checks/automatic/aria-role-name-present.js +47 -21
- package/src/checks/automatic/aria-roles-valid.js +22 -12
- package/src/checks/automatic/aria-valid-attr-value.js +28 -7
- package/src/checks/automatic/aria-valid-attr.js +17 -5
- package/src/checks/automatic/autocomplete-valid.js +74 -16
- package/src/checks/automatic/avoid-inline-spacing.js +20 -6
- package/src/checks/automatic/binary-control-name-present.js +60 -50
- package/src/checks/automatic/button-name-present.js +48 -18
- package/src/checks/automatic/bypass-blocks-present.js +42 -25
- package/src/checks/automatic/canvas-text-alternative-present.js +57 -26
- package/src/checks/automatic/combobox-name-present.js +38 -45
- package/src/checks/automatic/contrast-computable.js +361 -341
- package/src/checks/automatic/contrast-enhanced.js +487 -466
- package/src/checks/automatic/contrast-minimum.js +486 -465
- package/src/checks/automatic/css-orientation-lock.js +30 -8
- package/src/checks/automatic/definition-list-children-valid.js +40 -19
- package/src/checks/automatic/deprecated-elements-not-used.js +21 -7
- package/src/checks/automatic/dialog-name-present.js +37 -75
- package/src/checks/automatic/dlitem-parent-valid.js +23 -8
- package/src/checks/automatic/duplicate-id-aria.js +24 -6
- package/src/checks/automatic/embed-text-alternative-present.js +86 -35
- package/src/checks/automatic/form-control-programmatic-label-present.js +79 -196
- package/src/checks/automatic/form-control-single-label.js +47 -10
- package/src/checks/automatic/html-xml-lang-mismatch.js +34 -18
- package/src/checks/automatic/iframe-focusable-content.js +26 -11
- package/src/checks/automatic/iframe-name-present.js +31 -9
- package/src/checks/automatic/iframe-title-unique.js +29 -8
- package/src/checks/automatic/img-alt-present.js +47 -43
- package/src/checks/automatic/input-image-alt-present.js +143 -112
- package/src/checks/automatic/label-in-name.js +50 -22
- package/src/checks/automatic/language-page-present.js +109 -109
- package/src/checks/automatic/link-in-text-block.js +59 -19
- package/src/checks/automatic/link-name-present.js +45 -14
- package/src/checks/automatic/list-children-valid.js +26 -9
- package/src/checks/automatic/listbox-name-present.js +39 -19
- package/src/checks/automatic/listitem-parent-valid.js +18 -6
- package/src/checks/automatic/menuitem-name-present.js +39 -61
- package/src/checks/automatic/meta-refresh-no-exceptions.js +28 -7
- package/src/checks/automatic/meta-refresh-timing-absent.js +20 -6
- package/src/checks/automatic/meta-viewport-zoom-enabled.js +24 -7
- package/src/checks/automatic/meter-name-present.js +36 -33
- package/src/checks/automatic/nested-interactive-controls-absent.js +31 -10
- package/src/checks/automatic/object-text-alternative-present.js +91 -39
- package/src/checks/automatic/option-name-present.js +38 -21
- package/src/checks/automatic/page-title-present.js +17 -6
- package/src/checks/automatic/progressbar-name-present.js +41 -34
- package/src/checks/automatic/role-img-alt-present.js +209 -157
- package/src/checks/automatic/searchbox-name-present.js +39 -19
- package/src/checks/automatic/server-side-image-map-absent.js +23 -8
- package/src/checks/automatic/slider-name-present.js +40 -47
- package/src/checks/automatic/spinbutton-name-present.js +39 -19
- package/src/checks/automatic/summary-name-present.js +37 -17
- package/src/checks/automatic/svg-image-text-alternative-present.js +114 -47
- package/src/checks/automatic/svg-text-alternative-present.js +246 -226
- package/src/checks/automatic/tab-name-present.js +37 -60
- package/src/checks/automatic/table-headers-attr-valid.js +24 -8
- package/src/checks/automatic/table-th-has-data-cells.js +22 -8
- package/src/checks/automatic/target-size-minimum.js +118 -48
- package/src/checks/automatic/td-has-header.js +29 -11
- package/src/checks/automatic/textbox-name-present.js +39 -19
- package/src/checks/automatic/tooltip-name-present.js +37 -18
- package/src/checks/automatic/treeitem-name-present.js +38 -21
- package/src/checks/automatic/valid-lang.js +20 -6
- package/src/checks/automatic/video-poster-text-alternative-present.js +79 -36
- package/src/checks/manual/accesskeys-manual.js +14 -5
- package/src/checks/manual/area-alt-decorative-manual.js +192 -193
- package/src/checks/manual/area-alt-quality-manual.js +182 -141
- package/src/checks/manual/aria-checked-state-mismatch-manual.js +34 -11
- package/src/checks/manual/aria-text-manual.js +14 -6
- package/src/checks/manual/canvas-text-alternative-quality-manual.js +149 -114
- package/src/checks/manual/css-hidden-focus.js +196 -165
- package/src/checks/manual/embed-text-alternative-quality-manual.js +171 -160
- package/src/checks/manual/empty-heading-manual.js +24 -7
- package/src/checks/manual/empty-table-header-manual.js +17 -6
- package/src/checks/manual/focus-order-semantics-manual.js +45 -10
- package/src/checks/manual/form-control-programmatic-label-quality-manual.js +207 -246
- package/src/checks/manual/heading-order-manual.js +22 -7
- package/src/checks/manual/identical-links-same-purpose-manual.js +34 -12
- package/src/checks/manual/image-redundant-alt-manual.js +17 -7
- package/src/checks/manual/img-alt-decorative-manual.js +131 -96
- package/src/checks/manual/img-alt-quality-manual.js +176 -127
- package/src/checks/manual/input-image-alt-decorative-manual.js +125 -92
- package/src/checks/manual/input-image-alt-quality-manual.js +125 -92
- package/src/checks/manual/label-title-only-manual.js +14 -5
- package/src/checks/manual/landmark-banner-is-top-level-manual.js +66 -16
- package/src/checks/manual/landmark-contentinfo-is-top-level-manual.js +53 -16
- package/src/checks/manual/landmark-main-is-top-level-manual.js +35 -10
- package/src/checks/manual/landmark-no-duplicate-banner-manual.js +29 -10
- package/src/checks/manual/landmark-no-duplicate-contentinfo-manual.js +29 -10
- package/src/checks/manual/landmark-no-duplicate-main-manual.js +17 -8
- package/src/checks/manual/landmark-one-main-manual.js +26 -20
- package/src/checks/manual/landmark-unique-manual.js +41 -15
- package/src/checks/manual/link-name-quality-manual.js +43 -12
- package/src/checks/manual/media-transcript-present-manual.js +35 -22
- package/src/checks/manual/meta-viewport-large-manual.js +16 -5
- package/src/checks/manual/mouse-only-event-handlers-manual.js +38 -11
- package/src/checks/manual/no-autoplay-audio-manual.js +20 -6
- package/src/checks/manual/object-text-alternative-quality-manual.js +175 -154
- package/src/checks/manual/p-as-heading-manual.js +22 -7
- package/src/checks/manual/page-has-heading-one-manual.js +31 -22
- package/src/checks/manual/page-title-patterns-manual.js +78 -50
- package/src/checks/manual/presentation-role-conflict-manual.js +59 -19
- package/src/checks/manual/region-manual.js +241 -48
- package/src/checks/manual/scope-attr-valid-manual.js +10 -3
- package/src/checks/manual/scrollable-region-focusable-manual.js +37 -11
- package/src/checks/manual/skip-link-manual.js +35 -12
- package/src/checks/manual/svg-text-alternative-quality-manual.js +206 -165
- package/src/checks/manual/tabindex-manual.js +10 -3
- package/src/checks/manual/table-duplicate-name-manual.js +17 -7
- package/src/checks/manual/table-fake-caption-manual.js +24 -7
- package/src/checks/manual/video-caption-manual.js +15 -4
- package/src/checks/manual-review.js +56 -12
- package/src/core/aria-helpers.js +1127 -886
- package/src/core/contrast-helpers.js +1217 -1062
- package/src/core/dom-helpers.js +4175 -3917
- package/src/core/dom-runner.js +720 -604
- package/src/core/frame-messaging.js +189 -138
- package/src/core/frame-scan.js +94 -82
- package/src/core/rollup-composites.js +94 -102
- package/src/core/rule-meta.js +58 -41
- package/src/core.js +36552 -29111
- package/src/i18n/en.js +1194 -889
- package/src/i18n/fr.js +1136 -795
- package/src/policy/contracts.js +13 -13
- package/src/policy/resolvePolicy.js +48 -44
- package/src/report.js +63 -43
- package/src/sarif.js +175 -0
- package/surea11y.browser.js +36042 -0
package/src/policy/contracts.js
CHANGED
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
module.exports = {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
}
|
|
4
|
+
POLICY_CONTRACTS: {
|
|
5
|
+
a11y: {
|
|
6
|
+
id: 'a11y',
|
|
7
|
+
allowedOutcomes: ['fail', 'pass', 'cantTell', 'notApplicable'],
|
|
8
|
+
allowedConfidence: ['high', 'medium', 'low'],
|
|
9
|
+
coerceManualFailToCantTell: true
|
|
10
|
+
},
|
|
11
|
+
generic: {
|
|
12
|
+
id: 'generic',
|
|
13
|
+
allowedOutcomes: ['fail', 'pass', 'cantTell', 'notApplicable'],
|
|
14
|
+
allowedConfidence: ['high', 'medium', 'low'],
|
|
15
|
+
coerceManualFailToCantTell: false
|
|
17
16
|
}
|
|
17
|
+
}
|
|
18
18
|
};
|
|
@@ -1,55 +1,59 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
function resolvePolicy(POLICY_CONTRACTS, engineOptions) {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
return fallback;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
function normalizePolicyOverrides(policy) {
|
|
34
|
-
const p = (policy && typeof policy === 'object') ? policy : {};
|
|
35
|
-
return {
|
|
36
|
-
allowedOutcomes: Array.isArray(p.allowedOutcomes) ? p.allowedOutcomes.slice() : null,
|
|
37
|
-
allowedConfidence: Array.isArray(p.allowedConfidence) ? p.allowedConfidence.slice() : null,
|
|
38
|
-
coerceManualFailToCantTell: typeof p.coerceManualFailToCantTell === 'boolean' ? p.coerceManualFailToCantTell : null
|
|
39
|
-
};
|
|
4
|
+
function normalizePolicyContract(POLICY_CONTRACTS, contract, fallbackId) {
|
|
5
|
+
const fallback = POLICY_CONTRACTS[fallbackId] || POLICY_CONTRACTS.a11y;
|
|
6
|
+
if (typeof contract === 'string') return POLICY_CONTRACTS[contract] || fallback;
|
|
7
|
+
|
|
8
|
+
if (contract && typeof contract === 'object') {
|
|
9
|
+
const allowedOutcomes = Array.isArray(contract.allowedOutcomes)
|
|
10
|
+
? contract.allowedOutcomes.slice()
|
|
11
|
+
: fallback.allowedOutcomes.slice();
|
|
12
|
+
|
|
13
|
+
const allowedConfidence = Array.isArray(contract.allowedConfidence)
|
|
14
|
+
? contract.allowedConfidence.slice()
|
|
15
|
+
: fallback.allowedConfidence.slice();
|
|
16
|
+
|
|
17
|
+
return {
|
|
18
|
+
id:
|
|
19
|
+
typeof contract.id === 'string' && contract.id.trim()
|
|
20
|
+
? contract.id.trim()
|
|
21
|
+
: fallback.id || fallbackId || 'custom',
|
|
22
|
+
allowedOutcomes,
|
|
23
|
+
allowedConfidence,
|
|
24
|
+
coerceManualFailToCantTell:
|
|
25
|
+
typeof contract.coerceManualFailToCantTell === 'boolean'
|
|
26
|
+
? contract.coerceManualFailToCantTell
|
|
27
|
+
: !!fallback.coerceManualFailToCantTell
|
|
28
|
+
};
|
|
40
29
|
}
|
|
41
30
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
const ov = normalizePolicyOverrides(opts.policy);
|
|
31
|
+
return fallback;
|
|
32
|
+
}
|
|
45
33
|
|
|
34
|
+
function normalizePolicyOverrides(policy) {
|
|
35
|
+
const p = policy && typeof policy === 'object' ? policy : {};
|
|
46
36
|
return {
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
coerceManualFailToCantTell:
|
|
51
|
-
ov.coerceManualFailToCantTell !== null ? ov.coerceManualFailToCantTell : !!contract.coerceManualFailToCantTell
|
|
37
|
+
allowedOutcomes: Array.isArray(p.allowedOutcomes) ? p.allowedOutcomes.slice() : null,
|
|
38
|
+
allowedConfidence: Array.isArray(p.allowedConfidence) ? p.allowedConfidence.slice() : null,
|
|
39
|
+
coerceManualFailToCantTell:
|
|
40
|
+
typeof p.coerceManualFailToCantTell === 'boolean' ? p.coerceManualFailToCantTell : null
|
|
52
41
|
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const opts = engineOptions && typeof engineOptions === 'object' ? engineOptions : {};
|
|
45
|
+
const contract = normalizePolicyContract(POLICY_CONTRACTS, opts.policyContract, 'a11y');
|
|
46
|
+
const ov = normalizePolicyOverrides(opts.policy);
|
|
47
|
+
|
|
48
|
+
return {
|
|
49
|
+
contractId: contract.id,
|
|
50
|
+
allowedOutcomes: ov.allowedOutcomes || contract.allowedOutcomes.slice(),
|
|
51
|
+
allowedConfidence: ov.allowedConfidence || contract.allowedConfidence.slice(),
|
|
52
|
+
coerceManualFailToCantTell:
|
|
53
|
+
ov.coerceManualFailToCantTell !== null
|
|
54
|
+
? ov.coerceManualFailToCantTell
|
|
55
|
+
: !!contract.coerceManualFailToCantTell
|
|
56
|
+
};
|
|
53
57
|
}
|
|
54
58
|
|
|
55
59
|
module.exports = { resolvePolicy };
|
package/src/report.js
CHANGED
|
@@ -4,20 +4,13 @@
|
|
|
4
4
|
* Renders one scan result into a single, self-contained HTML report (no
|
|
5
5
|
* external CSS/JS/fonts -- opens straight from disk, no server, no network).
|
|
6
6
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* grouped "worth reviewing" cards with an overflow cap, collapsible
|
|
11
|
-
* "full technical data" with a scorecard + searchable/filterable/paginated
|
|
12
|
-
* table, dark-mode CSS) is generic and reusable; everything here is written
|
|
13
|
-
* fresh for a single-engine scan result, not copied, since that tool's own
|
|
14
|
-
* organizing principle (a 7-way "does surea11y vs axe-core agree" taxonomy)
|
|
15
|
-
* has no single-engine analog.
|
|
7
|
+
* Self-contained single file: hero bar + legend, grouped "worth reviewing"
|
|
8
|
+
* cards with an overflow cap, collapsible "full technical data" with a
|
|
9
|
+
* scorecard + searchable/filterable/paginated table, dark-mode CSS.
|
|
16
10
|
*/
|
|
17
11
|
|
|
18
|
-
//
|
|
19
|
-
//
|
|
20
|
-
// 4 outcomes, so no new palette is needed.
|
|
12
|
+
// Uses the dataviz skill's validated status palette, mapped 1:1 onto this
|
|
13
|
+
// engine's own 4 outcomes.
|
|
21
14
|
const STATUS = {
|
|
22
15
|
good: { color: '#0ca30c', bg: '#e9f7e9', icon: '✓' },
|
|
23
16
|
serious: { color: '#c1502e', bg: '#fdece5', icon: '⚠' },
|
|
@@ -26,8 +19,7 @@ const STATUS = {
|
|
|
26
19
|
};
|
|
27
20
|
|
|
28
21
|
// fail first: a QA tester scanning a report wants violations up front,
|
|
29
|
-
//
|
|
30
|
-
// of leading with violations, not passes.
|
|
22
|
+
// not passes.
|
|
31
23
|
const OUTCOME_ORDER = ['fail', 'cantTell', 'pass', 'notApplicable'];
|
|
32
24
|
const OUTCOME_INFO = {
|
|
33
25
|
fail: { label: 'Fail', status: 'serious', defaultOn: true },
|
|
@@ -40,7 +32,10 @@ for (const code of OUTCOME_ORDER) {
|
|
|
40
32
|
}
|
|
41
33
|
|
|
42
34
|
function esc(s) {
|
|
43
|
-
return String(s == null ? '' : s).replace(
|
|
35
|
+
return String(s == null ? '' : s).replace(
|
|
36
|
+
/[&<>"']/g,
|
|
37
|
+
(c) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' })[c]
|
|
38
|
+
);
|
|
44
39
|
}
|
|
45
40
|
|
|
46
41
|
// Neutralizes '<' so embedded JSON can never break out of its <script> tag,
|
|
@@ -69,8 +64,7 @@ function renderHeroBar(byOutcome) {
|
|
|
69
64
|
const total = OUTCOME_ORDER.reduce((sum, c) => sum + byOutcome[c], 0) || 1;
|
|
70
65
|
const applicable = byOutcome.pass + byOutcome.fail + byOutcome.cantTell;
|
|
71
66
|
|
|
72
|
-
const segments = OUTCOME_ORDER
|
|
73
|
-
.filter((c) => byOutcome[c] > 0)
|
|
67
|
+
const segments = OUTCOME_ORDER.filter((c) => byOutcome[c] > 0)
|
|
74
68
|
.map((c) => {
|
|
75
69
|
const n = byOutcome[c];
|
|
76
70
|
const pct = (n / total) * 100;
|
|
@@ -79,17 +73,15 @@ function renderHeroBar(byOutcome) {
|
|
|
79
73
|
})
|
|
80
74
|
.join('');
|
|
81
75
|
|
|
82
|
-
const legend = OUTCOME_ORDER
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
return `<div class="hero-legend-item">
|
|
76
|
+
const legend = OUTCOME_ORDER.map((c) => {
|
|
77
|
+
const n = byOutcome[c];
|
|
78
|
+
const info = OUTCOME_INFO[c];
|
|
79
|
+
return `<div class="hero-legend-item">
|
|
87
80
|
<span class="hero-legend-swatch" style="background:${info.color}">${info.icon}</span>
|
|
88
81
|
<span class="hero-legend-label">${esc(info.label)}</span>
|
|
89
82
|
<span class="hero-legend-count">${n.toLocaleString()} (${fmtPct(n, total)})</span>
|
|
90
83
|
</div>`;
|
|
91
|
-
|
|
92
|
-
.join('\n');
|
|
84
|
+
}).join('\n');
|
|
93
85
|
|
|
94
86
|
let headline;
|
|
95
87
|
if (byOutcome.fail > 0) {
|
|
@@ -134,7 +126,8 @@ function renderWcagRollup(rulesResults) {
|
|
|
134
126
|
|
|
135
127
|
const byLevel = { A: [], AA: [], AAA: [], other: [] };
|
|
136
128
|
for (const r of rulesResults) {
|
|
137
|
-
const mapping =
|
|
129
|
+
const mapping =
|
|
130
|
+
(r.meta && Array.isArray(r.meta.normativeMappings) && r.meta.normativeMappings[0]) || null;
|
|
138
131
|
const level = (mapping && mapping.level) || 'other';
|
|
139
132
|
(byLevel[level] || byLevel.other).push({ rule: r, mapping });
|
|
140
133
|
}
|
|
@@ -143,7 +136,13 @@ function renderWcagRollup(rulesResults) {
|
|
|
143
136
|
.filter((level) => byLevel[level].length)
|
|
144
137
|
.map((level) => {
|
|
145
138
|
const rows = byLevel[level]
|
|
146
|
-
.sort((a, b) =>
|
|
139
|
+
.sort((a, b) =>
|
|
140
|
+
(a.mapping ? a.mapping.requirement : '').localeCompare(
|
|
141
|
+
b.mapping ? b.mapping.requirement : '',
|
|
142
|
+
undefined,
|
|
143
|
+
{ numeric: true }
|
|
144
|
+
)
|
|
145
|
+
)
|
|
147
146
|
.map(({ rule, mapping }) => {
|
|
148
147
|
const info = OUTCOME_INFO[rule.outcome] || OUTCOME_INFO.notApplicable;
|
|
149
148
|
const metrics = (rule.data && rule.data.details && rule.data.details.metrics) || {};
|
|
@@ -176,8 +175,12 @@ function renderWcagRollup(rulesResults) {
|
|
|
176
175
|
// the findings table below shows the untruncated value.
|
|
177
176
|
const CARD_SNIPPET_MAX = 220;
|
|
178
177
|
function truncateForCard(s) {
|
|
179
|
-
const collapsed = String(s || '')
|
|
180
|
-
|
|
178
|
+
const collapsed = String(s || '')
|
|
179
|
+
.replace(/\s+/g, ' ')
|
|
180
|
+
.trim();
|
|
181
|
+
return collapsed.length > CARD_SNIPPET_MAX
|
|
182
|
+
? `${collapsed.slice(0, CARD_SNIPPET_MAX)}…`
|
|
183
|
+
: collapsed;
|
|
181
184
|
}
|
|
182
185
|
|
|
183
186
|
// One card per rule (not per occurrence) -- a rule with many occurrences of
|
|
@@ -185,7 +188,12 @@ function truncateForCard(s) {
|
|
|
185
188
|
const MAX_CARDS = 24;
|
|
186
189
|
|
|
187
190
|
function renderCards(checksResults) {
|
|
188
|
-
const withIssues = checksResults.filter(
|
|
191
|
+
const withIssues = checksResults.filter(
|
|
192
|
+
(r) =>
|
|
193
|
+
(r.outcome === 'fail' || r.outcome === 'cantTell') &&
|
|
194
|
+
Array.isArray(r.occurrences) &&
|
|
195
|
+
r.occurrences.length > 0
|
|
196
|
+
);
|
|
189
197
|
if (!withIssues.length) {
|
|
190
198
|
return '<p class="note">No fail/cantTell rules with occurrences on this scan.</p>';
|
|
191
199
|
}
|
|
@@ -196,15 +204,22 @@ function renderCards(checksResults) {
|
|
|
196
204
|
});
|
|
197
205
|
const shown = sorted.slice(0, MAX_CARDS);
|
|
198
206
|
|
|
199
|
-
const cards = shown
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
207
|
+
const cards = shown
|
|
208
|
+
.map((r) => {
|
|
209
|
+
const info = OUTCOME_INFO[r.outcome];
|
|
210
|
+
const occ = r.occurrences[0];
|
|
211
|
+
const wcagChips = ((r.meta && r.meta.normativeMappings) || [])
|
|
212
|
+
.map(
|
|
213
|
+
(m) =>
|
|
214
|
+
`<span class="chip" style="background:${STATUS.neutral.bg};color:${STATUS.neutral.color}">WCAG ${esc(m.requirement)}</span>`
|
|
215
|
+
)
|
|
216
|
+
.join('');
|
|
217
|
+
const countLabel =
|
|
218
|
+
r.occurrences.length > 1
|
|
219
|
+
? ` <span class="card-count">× ${r.occurrences.length.toLocaleString()}</span>`
|
|
220
|
+
: '';
|
|
221
|
+
|
|
222
|
+
return `<div class="card">
|
|
208
223
|
<div class="card-head">
|
|
209
224
|
<span class="hero-legend-swatch" style="background:${info.color}">${info.icon}</span>
|
|
210
225
|
<span class="card-title"><strong>${esc(r.ruleId)}</strong> (${esc(info.label)}, ${esc(r.severity)})${countLabel}</span>
|
|
@@ -216,11 +231,13 @@ function renderCards(checksResults) {
|
|
|
216
231
|
${r.occurrences.length > 1 ? `<p class="card-note">Selector/summary above are from one representative occurrence — ${r.occurrences.length.toLocaleString()} total on this rule.</p>` : ''}
|
|
217
232
|
</div>
|
|
218
233
|
</div>`;
|
|
219
|
-
|
|
234
|
+
})
|
|
235
|
+
.join('\n');
|
|
220
236
|
|
|
221
|
-
const overflow =
|
|
222
|
-
|
|
223
|
-
|
|
237
|
+
const overflow =
|
|
238
|
+
sorted.length > MAX_CARDS
|
|
239
|
+
? `<p class="note">Showing the ${MAX_CARDS} highest-priority rules of ${sorted.length.toLocaleString()} with issues — see the full technical data below for the rest.</p>`
|
|
240
|
+
: '';
|
|
224
241
|
|
|
225
242
|
return `<div class="cards">${cards}</div>${overflow}`;
|
|
226
243
|
}
|
|
@@ -251,7 +268,10 @@ function renderHtmlReport(result, options = {}) {
|
|
|
251
268
|
const checksResults = Array.isArray(result && result.checksResults) ? result.checksResults : [];
|
|
252
269
|
const rulesResults = Array.isArray(result && result.rulesResults) ? result.rulesResults : [];
|
|
253
270
|
const byOutcome = countByOutcome(checksResults);
|
|
254
|
-
const generatedAtLabel = new Date().toLocaleString('en-US', {
|
|
271
|
+
const generatedAtLabel = new Date().toLocaleString('en-US', {
|
|
272
|
+
dateStyle: 'medium',
|
|
273
|
+
timeStyle: 'medium'
|
|
274
|
+
});
|
|
255
275
|
const title = (options && options.title) || 'surea11y scan report';
|
|
256
276
|
|
|
257
277
|
const defaultOnList = OUTCOME_ORDER.filter((c) => OUTCOME_INFO[c].defaultOn);
|
package/src/sarif.js
ADDED
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Renders one scan result as a SARIF 2.1.0 log (docs/SARIF.md) for GitHub
|
|
5
|
+
* Code Scanning / other SARIF-consuming dashboards -- a different output
|
|
6
|
+
* shape from --json's raw result, purpose-built for that ecosystem (see
|
|
7
|
+
* docs/CLI.md's --sarif flag).
|
|
8
|
+
*
|
|
9
|
+
* Only fail/cantTell occurrences become SARIF results (a pass/notApplicable
|
|
10
|
+
* result has no occurrences at all -- see docs/OUTPUT_SCHEMA.md), the same
|
|
11
|
+
* "violations only" framing docs/REPORT.md's HTML report already uses.
|
|
12
|
+
* fail -> SARIF level "error" (the CI-gating case); cantTell -> "warning"
|
|
13
|
+
* (surfaced, non-blocking -- this engine's own cantTell/manual-review mental
|
|
14
|
+
* model, docs/TROUBLESHOOTING.md).
|
|
15
|
+
*
|
|
16
|
+
* If `baselineEntries` is supplied (mirrors --baseline, docs/BASELINE.md),
|
|
17
|
+
* fail occurrences already recorded there are omitted entirely rather than
|
|
18
|
+
* downgraded -- a generic SARIF consumer has no "known, don't re-gate"
|
|
19
|
+
* concept of its own, so the only faithful way to honor a baseline here is
|
|
20
|
+
* to not report the occurrence at all. cantTell occurrences are never
|
|
21
|
+
* baseline-filtered (the baseline mechanism only ever tracks fail
|
|
22
|
+
* occurrences, matching --write-baseline).
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
const path = require('path');
|
|
26
|
+
const { computeBaselineKey, getReasonCode } = require('./baseline.js');
|
|
27
|
+
|
|
28
|
+
const SARIF_SCHEMA_URI =
|
|
29
|
+
'https://raw.githubusercontent.com/oasis-tcs/sarif-spec/main/Schemata/sarif-schema-2.1.0.json';
|
|
30
|
+
const SARIF_VERSION = '2.1.0';
|
|
31
|
+
|
|
32
|
+
function artifactUriFromResult(result) {
|
|
33
|
+
const url = result && result.url;
|
|
34
|
+
if (!url) return 'about:blank';
|
|
35
|
+
if (url.startsWith('file://')) {
|
|
36
|
+
const filePath = url.slice('file://'.length);
|
|
37
|
+
const rel = path.relative(process.cwd(), filePath).split(path.sep).join('/');
|
|
38
|
+
// A file outside the cwd (e.g. an absolute path elsewhere on disk)
|
|
39
|
+
// produces a "../"-leading relative path -- still valid as a URI, just
|
|
40
|
+
// not resolvable as a repo-relative one by a SARIF consumer like GitHub
|
|
41
|
+
// Code Scanning (see docs/SARIF.md's known limitations).
|
|
42
|
+
return rel || filePath;
|
|
43
|
+
}
|
|
44
|
+
return url;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function buildRemainingBaselineMap(baselineEntries) {
|
|
48
|
+
const remaining = new Map();
|
|
49
|
+
for (const entry of Array.isArray(baselineEntries) ? baselineEntries : []) {
|
|
50
|
+
if (!entry) continue;
|
|
51
|
+
const key = computeBaselineKey(
|
|
52
|
+
entry.ruleId,
|
|
53
|
+
entry.reasonCode || 'DEFAULT',
|
|
54
|
+
typeof entry.html === 'string' ? entry.html : ''
|
|
55
|
+
);
|
|
56
|
+
remaining.set(key, (remaining.get(key) || 0) + 1);
|
|
57
|
+
}
|
|
58
|
+
return remaining;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function wcagTags(check) {
|
|
62
|
+
const mappings = (check.meta && check.meta.normativeMappings) || [];
|
|
63
|
+
const tags = new Set(['accessibility', check.type === 'automatic' ? 'automatic' : 'manual']);
|
|
64
|
+
for (const m of mappings) {
|
|
65
|
+
if (m && m.requirement) tags.add(`wcag-${m.requirement}`);
|
|
66
|
+
}
|
|
67
|
+
return Array.from(tags);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function buildRule(check) {
|
|
71
|
+
return {
|
|
72
|
+
id: check.ruleId,
|
|
73
|
+
name: check.ruleId,
|
|
74
|
+
shortDescription: { text: check.title || check.ruleId },
|
|
75
|
+
fullDescription: { text: check.description || check.title || check.ruleId },
|
|
76
|
+
// type: "manual" rules are capped at cantTell (never fail), so their
|
|
77
|
+
// worst-case, rule-level default is "warning"; automatic rules can
|
|
78
|
+
// reach "error" -- see docs/OUTPUT_SCHEMA.md's outcome/type table.
|
|
79
|
+
defaultConfiguration: { level: check.type === 'automatic' ? 'error' : 'warning' },
|
|
80
|
+
properties: { tags: wcagTags(check) }
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function buildResult(check, occurrence, level, artifactUri) {
|
|
85
|
+
const reasonCode = getReasonCode(occurrence);
|
|
86
|
+
const html = typeof occurrence.html === 'string' ? occurrence.html : '';
|
|
87
|
+
const message = occurrence.hint ? `${occurrence.summary} ${occurrence.hint}` : occurrence.summary;
|
|
88
|
+
|
|
89
|
+
return {
|
|
90
|
+
ruleId: check.ruleId,
|
|
91
|
+
level,
|
|
92
|
+
message: { text: message },
|
|
93
|
+
locations: [
|
|
94
|
+
{
|
|
95
|
+
physicalLocation: { artifactLocation: { uri: artifactUri } },
|
|
96
|
+
...(occurrence.selector
|
|
97
|
+
? { logicalLocations: [{ fullyQualifiedName: occurrence.selector, kind: 'element' }] }
|
|
98
|
+
: {})
|
|
99
|
+
}
|
|
100
|
+
],
|
|
101
|
+
partialFingerprints: {
|
|
102
|
+
'surea11y/violation/v1': computeBaselineKey(check.ruleId, reasonCode, html)
|
|
103
|
+
},
|
|
104
|
+
properties: {
|
|
105
|
+
severity: check.severity,
|
|
106
|
+
confidence: check.confidence,
|
|
107
|
+
reasonCode,
|
|
108
|
+
html
|
|
109
|
+
}
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function renderSarifReport(result, options = {}) {
|
|
114
|
+
const { toolVersion, informationUri, baselineEntries } = options;
|
|
115
|
+
const artifactUri = artifactUriFromResult(result);
|
|
116
|
+
const remaining = buildRemainingBaselineMap(baselineEntries);
|
|
117
|
+
|
|
118
|
+
const rules = [];
|
|
119
|
+
const seenRuleIds = new Set();
|
|
120
|
+
const failResults = [];
|
|
121
|
+
const cantTellResults = [];
|
|
122
|
+
|
|
123
|
+
for (const check of (result && result.checksResults) || []) {
|
|
124
|
+
if (!check || !Array.isArray(check.occurrences)) continue;
|
|
125
|
+
|
|
126
|
+
if (!seenRuleIds.has(check.ruleId)) {
|
|
127
|
+
seenRuleIds.add(check.ruleId);
|
|
128
|
+
rules.push(buildRule(check));
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (check.outcome !== 'fail' && check.outcome !== 'cantTell') continue;
|
|
132
|
+
|
|
133
|
+
for (const occurrence of check.occurrences) {
|
|
134
|
+
if (!occurrence) continue;
|
|
135
|
+
|
|
136
|
+
if (check.outcome === 'fail') {
|
|
137
|
+
const reasonCode = getReasonCode(occurrence);
|
|
138
|
+
const html = typeof occurrence.html === 'string' ? occurrence.html : '';
|
|
139
|
+
const key = computeBaselineKey(check.ruleId, reasonCode, html);
|
|
140
|
+
const left = remaining.get(key) || 0;
|
|
141
|
+
if (left > 0) {
|
|
142
|
+
remaining.set(key, left - 1);
|
|
143
|
+
continue; // already known via the baseline -- omit, don't re-gate
|
|
144
|
+
}
|
|
145
|
+
failResults.push(buildResult(check, occurrence, 'error', artifactUri));
|
|
146
|
+
} else {
|
|
147
|
+
cantTellResults.push(buildResult(check, occurrence, 'warning', artifactUri));
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const sarifLog = {
|
|
153
|
+
$schema: SARIF_SCHEMA_URI,
|
|
154
|
+
version: SARIF_VERSION,
|
|
155
|
+
runs: [
|
|
156
|
+
{
|
|
157
|
+
tool: {
|
|
158
|
+
driver: {
|
|
159
|
+
name: 'surea11y',
|
|
160
|
+
informationUri: informationUri || 'https://github.com/rumoroso/surea11y-core',
|
|
161
|
+
version: toolVersion || '0.0.0',
|
|
162
|
+
rules
|
|
163
|
+
}
|
|
164
|
+
},
|
|
165
|
+
// fail first: matches docs/REPORT.md's own "violations before advisory
|
|
166
|
+
// findings" ordering.
|
|
167
|
+
results: [...failResults, ...cantTellResults]
|
|
168
|
+
}
|
|
169
|
+
]
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
return JSON.stringify(sarifLog, null, 2) + '\n';
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
module.exports = { renderSarifReport };
|