lighthouse 12.2.3 → 12.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/cli/test/smokehouse/core-tests.js +8 -0
- package/core/audits/byte-efficiency/render-blocking-resources.js +1 -1
- package/core/audits/has-hsts.d.ts +44 -0
- package/core/audits/has-hsts.js +208 -0
- package/core/audits/origin-isolation.d.ts +40 -0
- package/core/audits/origin-isolation.js +155 -0
- package/core/computed/metrics/cumulative-layout-shift.js +4 -4
- package/core/computed/metrics/lantern-metric.js +4 -2
- package/core/computed/navigation-insights.d.ts +1 -1
- package/core/computed/network-analysis.js +13 -1
- package/core/computed/trace-engine-result.d.ts +4 -0
- package/core/computed/trace-engine-result.js +30 -4
- package/core/config/default-config.js +4 -0
- package/core/gather/gatherers/seo/font-size.d.ts +1 -0
- package/core/gather/gatherers/seo/font-size.js +20 -11
- package/core/lib/trace-engine.d.ts +1 -1
- package/core/lib/trace-engine.js +1 -1
- package/package.json +4 -4
- package/shared/localization/locales/ar-XB.json +60 -0
- package/shared/localization/locales/ar.json +60 -0
- package/shared/localization/locales/bg.json +77 -17
- package/shared/localization/locales/ca.json +77 -17
- package/shared/localization/locales/cs.json +77 -17
- package/shared/localization/locales/da.json +77 -17
- package/shared/localization/locales/de.json +77 -17
- package/shared/localization/locales/el.json +77 -17
- package/shared/localization/locales/en-GB.json +70 -10
- package/shared/localization/locales/en-US.json +154 -40
- package/shared/localization/locales/en-XL.json +154 -40
- package/shared/localization/locales/es-419.json +77 -17
- package/shared/localization/locales/es.json +60 -0
- package/shared/localization/locales/fi.json +60 -0
- package/shared/localization/locales/fil.json +60 -0
- package/shared/localization/locales/fr.json +77 -17
- package/shared/localization/locales/he.json +77 -17
- package/shared/localization/locales/hi.json +77 -17
- package/shared/localization/locales/hr.json +60 -0
- package/shared/localization/locales/hu.json +77 -17
- package/shared/localization/locales/id.json +77 -17
- package/shared/localization/locales/it.json +77 -17
- package/shared/localization/locales/ja.json +60 -0
- package/shared/localization/locales/ko.json +60 -0
- package/shared/localization/locales/lt.json +77 -17
- package/shared/localization/locales/lv.json +77 -17
- package/shared/localization/locales/nl.json +60 -0
- package/shared/localization/locales/no.json +77 -17
- package/shared/localization/locales/pl.json +60 -0
- package/shared/localization/locales/pt-PT.json +60 -0
- package/shared/localization/locales/pt.json +77 -17
- package/shared/localization/locales/ro.json +77 -17
- package/shared/localization/locales/ru.json +60 -0
- package/shared/localization/locales/sk.json +60 -0
- package/shared/localization/locales/sl.json +77 -17
- package/shared/localization/locales/sr-Latn.json +60 -0
- package/shared/localization/locales/sr.json +60 -0
- package/shared/localization/locales/sv.json +77 -17
- package/shared/localization/locales/ta.json +77 -17
- package/shared/localization/locales/te.json +72 -12
- package/shared/localization/locales/th.json +60 -0
- package/shared/localization/locales/tr.json +77 -17
- package/shared/localization/locales/uk.json +78 -18
- package/shared/localization/locales/vi.json +60 -0
- package/shared/localization/locales/zh-HK.json +77 -17
- package/shared/localization/locales/zh-TW.json +77 -17
- package/shared/localization/locales/zh.json +77 -17
- package/types/artifacts.d.ts +2 -2
|
@@ -21,6 +21,8 @@ import fpsMaxPassive from './test-definitions/fps-max-passive.js';
|
|
|
21
21
|
import fpsScaled from './test-definitions/fps-scaled.js';
|
|
22
22
|
import fpsOverflowX from './test-definitions/fps-overflow-x.js';
|
|
23
23
|
import issuesMixedContent from './test-definitions/issues-mixed-content.js';
|
|
24
|
+
import hstsFullyPresent from './test-definitions/hsts-fully-present.js';
|
|
25
|
+
import hstsMissingDirectives from './test-definitions/hsts-missing-directives.js';
|
|
24
26
|
import lanternFetch from './test-definitions/lantern-fetch.js';
|
|
25
27
|
import lanternIdleCallbackLong from './test-definitions/lantern-idle-callback-long.js';
|
|
26
28
|
import lanternIdleCallbackShort from './test-definitions/lantern-idle-callback-short.js';
|
|
@@ -35,6 +37,8 @@ import metricsTrickyTti from './test-definitions/metrics-tricky-tti.js';
|
|
|
35
37
|
import metricsTrickyTtiLateFcp from './test-definitions/metrics-tricky-tti-late-fcp.js';
|
|
36
38
|
import oopifRequests from './test-definitions/oopif-requests.js';
|
|
37
39
|
import oopifScripts from './test-definitions/oopif-scripts.js';
|
|
40
|
+
import originIsolationCoopHeaderMissing from './test-definitions/origin-isolation-coop-header-missing.js';
|
|
41
|
+
import originIsolationCoopPresent from './test-definitions/origin-isolation-coop-present.js';
|
|
38
42
|
import perfDebug from './test-definitions/perf-debug.js';
|
|
39
43
|
import perfDiagnosticsAnimations from './test-definitions/perf-diagnostics-animations.js';
|
|
40
44
|
import perfDiagnosticsThirdParty from './test-definitions/perf-diagnostics-third-party.js';
|
|
@@ -79,6 +83,8 @@ const smokeTests = [
|
|
|
79
83
|
fpsOverflowX,
|
|
80
84
|
fpsScaled,
|
|
81
85
|
issuesMixedContent,
|
|
86
|
+
hstsFullyPresent,
|
|
87
|
+
hstsMissingDirectives,
|
|
82
88
|
lanternFetch,
|
|
83
89
|
lanternIdleCallbackLong,
|
|
84
90
|
lanternIdleCallbackShort,
|
|
@@ -93,6 +99,8 @@ const smokeTests = [
|
|
|
93
99
|
metricsTrickyTtiLateFcp,
|
|
94
100
|
oopifRequests,
|
|
95
101
|
oopifScripts,
|
|
102
|
+
originIsolationCoopHeaderMissing,
|
|
103
|
+
originIsolationCoopPresent,
|
|
96
104
|
perfDebug,
|
|
97
105
|
perfDiagnosticsAnimations,
|
|
98
106
|
perfDiagnosticsThirdParty,
|
|
@@ -130,7 +130,7 @@ class RenderBlockingResources extends Audit {
|
|
|
130
130
|
const wastedCssBytes = await RenderBlockingResources.computeWastedCSSBytes(artifacts, context);
|
|
131
131
|
const navInsights = await NavigationInsights.request(trace, context);
|
|
132
132
|
|
|
133
|
-
const renderBlocking = navInsights.RenderBlocking;
|
|
133
|
+
const renderBlocking = navInsights.model.RenderBlocking;
|
|
134
134
|
if (renderBlocking instanceof Error) throw renderBlocking;
|
|
135
135
|
|
|
136
136
|
/** @type {LH.Audit.Context['settings']} */
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
export default HasHsts;
|
|
2
|
+
declare class HasHsts extends Audit {
|
|
3
|
+
/**
|
|
4
|
+
* @param {LH.Artifacts} artifacts
|
|
5
|
+
* @param {LH.Audit.Context} context
|
|
6
|
+
* @return {Promise<string[]>}
|
|
7
|
+
*/
|
|
8
|
+
static getRawHsts(artifacts: LH.Artifacts, context: LH.Audit.Context): Promise<string[]>;
|
|
9
|
+
/**
|
|
10
|
+
* @param {string} hstsDirective
|
|
11
|
+
* @param {LH.IcuMessage | string} findingDescription
|
|
12
|
+
* @param {LH.IcuMessage=} severity
|
|
13
|
+
* @return {LH.Audit.Details.TableItem}
|
|
14
|
+
*/
|
|
15
|
+
static findingToTableItem(hstsDirective: string, findingDescription: LH.IcuMessage | string, severity?: LH.IcuMessage | undefined): LH.Audit.Details.TableItem;
|
|
16
|
+
/**
|
|
17
|
+
* @param {string[]} hstsHeaders
|
|
18
|
+
* @return {{score: number, results: LH.Audit.Details.TableItem[]}}
|
|
19
|
+
*/
|
|
20
|
+
static constructResults(hstsHeaders: string[]): {
|
|
21
|
+
score: number;
|
|
22
|
+
results: LH.Audit.Details.TableItem[];
|
|
23
|
+
};
|
|
24
|
+
/**
|
|
25
|
+
* @param {LH.Artifacts} artifacts
|
|
26
|
+
* @param {LH.Audit.Context} context
|
|
27
|
+
* @return {Promise<LH.Audit.Product>}
|
|
28
|
+
*/
|
|
29
|
+
static audit(artifacts: LH.Artifacts, context: LH.Audit.Context): Promise<LH.Audit.Product>;
|
|
30
|
+
}
|
|
31
|
+
export namespace UIStrings {
|
|
32
|
+
let title: string;
|
|
33
|
+
let description: string;
|
|
34
|
+
let noHsts: string;
|
|
35
|
+
let noPreload: string;
|
|
36
|
+
let noSubdomain: string;
|
|
37
|
+
let noMaxAge: string;
|
|
38
|
+
let lowMaxAge: string;
|
|
39
|
+
let invalidSyntax: string;
|
|
40
|
+
let columnDirective: string;
|
|
41
|
+
let columnSeverity: string;
|
|
42
|
+
}
|
|
43
|
+
import { Audit } from './audit.js';
|
|
44
|
+
//# sourceMappingURL=has-hsts.d.ts.map
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2024 Google LLC
|
|
4
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import {Audit} from './audit.js';
|
|
8
|
+
import {MainResource} from '../computed/main-resource.js';
|
|
9
|
+
import * as i18n from '../lib/i18n/i18n.js';
|
|
10
|
+
|
|
11
|
+
const UIStrings = {
|
|
12
|
+
/** Title of a Lighthouse audit that evaluates the security of a page's HSTS header. "HSTS" stands for "HTTP Strict Transport Security". */
|
|
13
|
+
title: 'Use a strong HSTS policy',
|
|
14
|
+
/** Description of a Lighthouse audit that evaluates the security of a page's HSTS header. This is displayed after a user expands the section to see more. No character length limits. The last sentence starting with 'Learn' becomes link text to additional documentation. "HSTS" stands for "HTTP Strict Transport Security". */
|
|
15
|
+
description: 'Deployment of the HSTS header significantly ' +
|
|
16
|
+
'reduces the risk of downgrading HTTP connections and eavesdropping attacks. ' +
|
|
17
|
+
'A rollout in stages, starting with a low max-age is recommended. ' +
|
|
18
|
+
'[Learn more about using a strong HSTS policy.](https://developer.chrome.com/docs/lighthouse/best-practices/has-hsts)',
|
|
19
|
+
/** Summary text for the results of a Lighthouse audit that evaluates the HSTS header. This is displayed if no HSTS header is deployed. "HSTS" stands for "HTTP Strict Transport Security". */
|
|
20
|
+
noHsts: 'No HSTS header found',
|
|
21
|
+
/** Summary text for the results of a Lighthouse audit that evaluates the HSTS header. This is displayed if the preload directive is missing. "HSTS" stands for "HTTP Strict Transport Security". */
|
|
22
|
+
noPreload: 'No `preload` directive found',
|
|
23
|
+
/** Summary text for the results of a Lighthouse audit that evaluates the HSTS header. This is displayed if the includeSubDomains directive is missing. "HSTS" stands for "HTTP Strict Transport Security". */
|
|
24
|
+
noSubdomain: 'No `includeSubDomains` directive found',
|
|
25
|
+
/** Summary text for the results of a Lighthouse audit that evaluates the HSTS header. This is displayed if the max-age directive is missing. "HSTS" stands for "HTTP Strict Transport Security". */
|
|
26
|
+
noMaxAge: 'No `max-age` directive',
|
|
27
|
+
/** Summary text for the results of a Lighthouse audit that evaluates the HSTS header. This is displayed if the provided duration for the max-age directive is too low. "HSTS" stands for "HTTP Strict Transport Security". */
|
|
28
|
+
lowMaxAge: '`max-age` is too low',
|
|
29
|
+
/** Table item value calling out the presence of a syntax error. */
|
|
30
|
+
invalidSyntax: 'Invalid syntax',
|
|
31
|
+
/** Label for a column in a data table; entries will be a directive of the HSTS header. "HSTS" stands for "HTTP Strict Transport Security". */
|
|
32
|
+
columnDirective: 'Directive',
|
|
33
|
+
/** Label for a column in a data table; entries will be the severity of an issue with the HSTS header. "HSTS" stands for "HTTP Strict Transport Security". */
|
|
34
|
+
columnSeverity: 'Severity',
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const str_ = i18n.createIcuMessageFn(import.meta.url, UIStrings);
|
|
38
|
+
|
|
39
|
+
class HasHsts extends Audit {
|
|
40
|
+
/**
|
|
41
|
+
* @return {LH.Audit.Meta}
|
|
42
|
+
*/
|
|
43
|
+
static get meta() {
|
|
44
|
+
return {
|
|
45
|
+
id: 'has-hsts',
|
|
46
|
+
scoreDisplayMode: Audit.SCORING_MODES.INFORMATIVE,
|
|
47
|
+
title: str_(UIStrings.title),
|
|
48
|
+
description: str_(UIStrings.description),
|
|
49
|
+
requiredArtifacts: ['devtoolsLogs', 'URL'],
|
|
50
|
+
supportedModes: ['navigation'],
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* @param {LH.Artifacts} artifacts
|
|
57
|
+
* @param {LH.Audit.Context} context
|
|
58
|
+
* @return {Promise<string[]>}
|
|
59
|
+
*/
|
|
60
|
+
static async getRawHsts(artifacts, context) {
|
|
61
|
+
const devtoolsLog = artifacts.devtoolsLogs[Audit.DEFAULT_PASS];
|
|
62
|
+
const mainResource =
|
|
63
|
+
await MainResource.request({devtoolsLog, URL: artifacts.URL}, context);
|
|
64
|
+
|
|
65
|
+
let hstsHeaders =
|
|
66
|
+
mainResource.responseHeaders
|
|
67
|
+
.filter(h => {
|
|
68
|
+
return h.name.toLowerCase() === 'strict-transport-security';
|
|
69
|
+
})
|
|
70
|
+
.flatMap(h => h.value.split(';'));
|
|
71
|
+
|
|
72
|
+
// Sanitize the header value / directives.
|
|
73
|
+
hstsHeaders = hstsHeaders.map(v => v.toLowerCase().replace(/\s/g, ''));
|
|
74
|
+
|
|
75
|
+
return hstsHeaders;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* @param {string} hstsDirective
|
|
80
|
+
* @param {LH.IcuMessage | string} findingDescription
|
|
81
|
+
* @param {LH.IcuMessage=} severity
|
|
82
|
+
* @return {LH.Audit.Details.TableItem}
|
|
83
|
+
*/
|
|
84
|
+
static findingToTableItem(hstsDirective, findingDescription, severity) {
|
|
85
|
+
return {
|
|
86
|
+
directive: hstsDirective,
|
|
87
|
+
description: findingDescription,
|
|
88
|
+
severity,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* @param {string[]} hstsHeaders
|
|
94
|
+
* @return {{score: number, results: LH.Audit.Details.TableItem[]}}
|
|
95
|
+
*/
|
|
96
|
+
static constructResults(hstsHeaders) {
|
|
97
|
+
const rawHsts = [...hstsHeaders];
|
|
98
|
+
const allowedDirectives = ['max-age', 'includesubdomains', 'preload'];
|
|
99
|
+
const violations = [];
|
|
100
|
+
const warnings = [];
|
|
101
|
+
const syntax = [];
|
|
102
|
+
|
|
103
|
+
if (!rawHsts.length) {
|
|
104
|
+
return {
|
|
105
|
+
score: 0,
|
|
106
|
+
results: [{
|
|
107
|
+
severity: str_(i18n.UIStrings.itemSeverityHigh),
|
|
108
|
+
description: str_(UIStrings.noHsts),
|
|
109
|
+
directive: undefined,
|
|
110
|
+
}],
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// No max-age is a violation and renders the HSTS header useless.
|
|
115
|
+
if (!hstsHeaders.toString().includes('max-age')) {
|
|
116
|
+
violations.push({
|
|
117
|
+
severity: str_(i18n.UIStrings.itemSeverityHigh),
|
|
118
|
+
description: str_(UIStrings.noMaxAge),
|
|
119
|
+
directive: 'max-age',
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (!hstsHeaders.toString().includes('includesubdomains')) {
|
|
124
|
+
// No includeSubdomains might be even wanted. But would be preferred.
|
|
125
|
+
warnings.push({
|
|
126
|
+
severity: str_(i18n.UIStrings.itemSeverityMedium),
|
|
127
|
+
description: str_(UIStrings.noSubdomain),
|
|
128
|
+
directive: 'includeSubDomains',
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (!hstsHeaders.toString().includes('preload')) {
|
|
133
|
+
// No preload might be even wanted. But would be preferred.
|
|
134
|
+
warnings.push({
|
|
135
|
+
severity: str_(i18n.UIStrings.itemSeverityMedium),
|
|
136
|
+
description: str_(UIStrings.noPreload),
|
|
137
|
+
directive: 'preload',
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
for (const actualDirective of hstsHeaders) {
|
|
142
|
+
// We recommend 2y max-age. But if it's lower than 1y, it's a violation.
|
|
143
|
+
if (actualDirective.includes('max-age') &&
|
|
144
|
+
parseInt(actualDirective.split('=')[1], 10) < 31536000) {
|
|
145
|
+
violations.push({
|
|
146
|
+
severity: str_(i18n.UIStrings.itemSeverityHigh),
|
|
147
|
+
description: str_(UIStrings.lowMaxAge),
|
|
148
|
+
directive: 'max-age',
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// If there is a directive that's not an official HSTS directive.
|
|
153
|
+
if (!allowedDirectives.includes(actualDirective) &&
|
|
154
|
+
!actualDirective.includes('max-age')) {
|
|
155
|
+
syntax.push({
|
|
156
|
+
severity: str_(i18n.UIStrings.itemSeverityLow),
|
|
157
|
+
description: str_(UIStrings.invalidSyntax),
|
|
158
|
+
directive: actualDirective,
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const results = [
|
|
164
|
+
...violations.map(
|
|
165
|
+
f => this.findingToTableItem(
|
|
166
|
+
f.directive, f.description,
|
|
167
|
+
str_(i18n.UIStrings.itemSeverityHigh))),
|
|
168
|
+
...warnings.map(
|
|
169
|
+
f => this.findingToTableItem(
|
|
170
|
+
f.directive, f.description,
|
|
171
|
+
str_(i18n.UIStrings.itemSeverityMedium))),
|
|
172
|
+
...syntax.map(
|
|
173
|
+
f => this.findingToTableItem(
|
|
174
|
+
f.directive, f.description,
|
|
175
|
+
str_(i18n.UIStrings.itemSeverityLow))),
|
|
176
|
+
];
|
|
177
|
+
return {score: violations.length || syntax.length ? 0 : 1, results};
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* @param {LH.Artifacts} artifacts
|
|
182
|
+
* @param {LH.Audit.Context} context
|
|
183
|
+
* @return {Promise<LH.Audit.Product>}
|
|
184
|
+
*/
|
|
185
|
+
static async audit(artifacts, context) {
|
|
186
|
+
const hstsHeaders = await this.getRawHsts(artifacts, context);
|
|
187
|
+
const {score, results} = this.constructResults(hstsHeaders);
|
|
188
|
+
|
|
189
|
+
/** @type {LH.Audit.Details.Table['headings']} */
|
|
190
|
+
const headings = [
|
|
191
|
+
/* eslint-disable max-len */
|
|
192
|
+
{key: 'description', valueType: 'text', subItemsHeading: {key: 'description'}, label: str_(i18n.UIStrings.columnDescription)},
|
|
193
|
+
{key: 'directive', valueType: 'code', subItemsHeading: {key: 'directive'}, label: str_(UIStrings.columnDirective)},
|
|
194
|
+
{key: 'severity', valueType: 'text', subItemsHeading: {key: 'severity'}, label: str_(UIStrings.columnSeverity)},
|
|
195
|
+
/* eslint-enable max-len */
|
|
196
|
+
];
|
|
197
|
+
const details = Audit.makeTableDetails(headings, results);
|
|
198
|
+
|
|
199
|
+
return {
|
|
200
|
+
score,
|
|
201
|
+
notApplicable: !results.length,
|
|
202
|
+
details,
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
export default HasHsts;
|
|
208
|
+
export {UIStrings};
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
export default OriginIsolation;
|
|
2
|
+
declare class OriginIsolation extends Audit {
|
|
3
|
+
/**
|
|
4
|
+
* @param {LH.Artifacts} artifacts
|
|
5
|
+
* @param {LH.Audit.Context} context
|
|
6
|
+
* @return {Promise<string[]>}
|
|
7
|
+
*/
|
|
8
|
+
static getRawCoop(artifacts: LH.Artifacts, context: LH.Audit.Context): Promise<string[]>;
|
|
9
|
+
/**
|
|
10
|
+
* @param {string | undefined} coopDirective
|
|
11
|
+
* @param {LH.IcuMessage | string} findingDescription
|
|
12
|
+
* @param {LH.IcuMessage=} severity
|
|
13
|
+
* @return {LH.Audit.Details.TableItem}
|
|
14
|
+
*/
|
|
15
|
+
static findingToTableItem(coopDirective: string | undefined, findingDescription: LH.IcuMessage | string, severity?: LH.IcuMessage | undefined): LH.Audit.Details.TableItem;
|
|
16
|
+
/**
|
|
17
|
+
* @param {string[]} coopHeaders
|
|
18
|
+
* @return {{score: number, results: LH.Audit.Details.TableItem[]}}
|
|
19
|
+
*/
|
|
20
|
+
static constructResults(coopHeaders: string[]): {
|
|
21
|
+
score: number;
|
|
22
|
+
results: LH.Audit.Details.TableItem[];
|
|
23
|
+
};
|
|
24
|
+
/**
|
|
25
|
+
* @param {LH.Artifacts} artifacts
|
|
26
|
+
* @param {LH.Audit.Context} context
|
|
27
|
+
* @return {Promise<LH.Audit.Product>}
|
|
28
|
+
*/
|
|
29
|
+
static audit(artifacts: LH.Artifacts, context: LH.Audit.Context): Promise<LH.Audit.Product>;
|
|
30
|
+
}
|
|
31
|
+
export namespace UIStrings {
|
|
32
|
+
let title: string;
|
|
33
|
+
let description: string;
|
|
34
|
+
let noCoop: string;
|
|
35
|
+
let invalidSyntax: string;
|
|
36
|
+
let columnDirective: string;
|
|
37
|
+
let columnSeverity: string;
|
|
38
|
+
}
|
|
39
|
+
import { Audit } from './audit.js';
|
|
40
|
+
//# sourceMappingURL=origin-isolation.d.ts.map
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2024 Google LLC
|
|
4
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import {Audit} from './audit.js';
|
|
8
|
+
import {MainResource} from '../computed/main-resource.js';
|
|
9
|
+
import * as i18n from '../lib/i18n/i18n.js';
|
|
10
|
+
|
|
11
|
+
const UIStrings = {
|
|
12
|
+
/** Title of a Lighthouse audit that evaluates the security of a page's COOP header for origin isolation. "COOP" stands for "Cross-Origin-Opener-Policy" and should not be translated. */
|
|
13
|
+
title: 'Ensure proper origin isolation with COOP',
|
|
14
|
+
/** Description of a Lighthouse audit that evaluates the security of a page's COOP header for origin isolation. This is displayed after a user expands the section to see more. No character length limits. The last sentence starting with 'Learn' becomes link text to additional documentation. "COOP" stands for "Cross-Origin-Opener-Policy", neither should be translated. */
|
|
15
|
+
description: 'The Cross-Origin-Opener-Policy (COOP) can be used to isolate the top-level window from other documents such as pop-ups. [Learn more about deploying the COOP header.](https://web.dev/articles/why-coop-coep#coop)',
|
|
16
|
+
/** Summary text for the results of a Lighthouse audit that evaluates the COOP header for origin isolation. This is displayed if no COOP header is deployed. "COOP" stands for "Cross-Origin-Opener-Policy" and should not be translated. */
|
|
17
|
+
noCoop: 'No COOP header found',
|
|
18
|
+
/** Table item value calling out the presence of a syntax error. */
|
|
19
|
+
invalidSyntax: 'Invalid syntax',
|
|
20
|
+
/** Label for a column in a data table; entries will be a directive of the COOP header. "COOP" stands for "Cross-Origin-Opener-Policy". */
|
|
21
|
+
columnDirective: 'Directive',
|
|
22
|
+
/** Label for a column in a data table; entries will be the severity of an issue with the COOP header. "COOP" stands for "Cross-Origin-Opener-Policy". */
|
|
23
|
+
columnSeverity: 'Severity',
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const str_ = i18n.createIcuMessageFn(import.meta.url, UIStrings);
|
|
27
|
+
|
|
28
|
+
class OriginIsolation extends Audit {
|
|
29
|
+
/**
|
|
30
|
+
* @return {LH.Audit.Meta}
|
|
31
|
+
*/
|
|
32
|
+
static get meta() {
|
|
33
|
+
return {
|
|
34
|
+
id: 'origin-isolation',
|
|
35
|
+
scoreDisplayMode: Audit.SCORING_MODES.INFORMATIVE,
|
|
36
|
+
title: str_(UIStrings.title),
|
|
37
|
+
description: str_(UIStrings.description),
|
|
38
|
+
requiredArtifacts: ['devtoolsLogs', 'URL'],
|
|
39
|
+
supportedModes: ['navigation'],
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* @param {LH.Artifacts} artifacts
|
|
46
|
+
* @param {LH.Audit.Context} context
|
|
47
|
+
* @return {Promise<string[]>}
|
|
48
|
+
*/
|
|
49
|
+
static async getRawCoop(artifacts, context) {
|
|
50
|
+
const devtoolsLog = artifacts.devtoolsLogs[Audit.DEFAULT_PASS];
|
|
51
|
+
const mainResource =
|
|
52
|
+
await MainResource.request({devtoolsLog, URL: artifacts.URL}, context);
|
|
53
|
+
|
|
54
|
+
let coopHeaders =
|
|
55
|
+
mainResource.responseHeaders
|
|
56
|
+
.filter(h => {
|
|
57
|
+
return h.name.toLowerCase() === 'cross-origin-opener-policy';
|
|
58
|
+
})
|
|
59
|
+
.flatMap(h => h.value);
|
|
60
|
+
|
|
61
|
+
// Sanitize the header value.
|
|
62
|
+
coopHeaders = coopHeaders.map(v => v.toLowerCase().replace(/\s/g, ''));
|
|
63
|
+
|
|
64
|
+
return coopHeaders;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* @param {string | undefined} coopDirective
|
|
69
|
+
* @param {LH.IcuMessage | string} findingDescription
|
|
70
|
+
* @param {LH.IcuMessage=} severity
|
|
71
|
+
* @return {LH.Audit.Details.TableItem}
|
|
72
|
+
*/
|
|
73
|
+
static findingToTableItem(coopDirective, findingDescription, severity) {
|
|
74
|
+
return {
|
|
75
|
+
directive: coopDirective,
|
|
76
|
+
description: findingDescription,
|
|
77
|
+
severity,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* @param {string[]} coopHeaders
|
|
83
|
+
* @return {{score: number, results: LH.Audit.Details.TableItem[]}}
|
|
84
|
+
*/
|
|
85
|
+
static constructResults(coopHeaders) {
|
|
86
|
+
const rawCoop = [...coopHeaders];
|
|
87
|
+
const allowedDirectives = [
|
|
88
|
+
'unsafe-none', 'same-origin-allow-popups', 'same-origin',
|
|
89
|
+
'noopener-allow-popups',
|
|
90
|
+
];
|
|
91
|
+
const violations = [];
|
|
92
|
+
const syntax = [];
|
|
93
|
+
|
|
94
|
+
if (!rawCoop.length) {
|
|
95
|
+
violations.push({
|
|
96
|
+
severity: str_(i18n.UIStrings.itemSeverityHigh),
|
|
97
|
+
description: str_(UIStrings.noCoop),
|
|
98
|
+
directive: undefined,
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
for (const actualDirective of coopHeaders) {
|
|
103
|
+
// If there is a directive that's not an official COOP directive.
|
|
104
|
+
if (!allowedDirectives.includes(actualDirective)) {
|
|
105
|
+
syntax.push({
|
|
106
|
+
severity: str_(i18n.UIStrings.itemSeverityLow),
|
|
107
|
+
description: str_(UIStrings.invalidSyntax),
|
|
108
|
+
directive: actualDirective,
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const results = [
|
|
114
|
+
...violations.map(
|
|
115
|
+
f => this.findingToTableItem(
|
|
116
|
+
f.directive, f.description,
|
|
117
|
+
str_(i18n.UIStrings.itemSeverityHigh))),
|
|
118
|
+
...syntax.map(
|
|
119
|
+
f => this.findingToTableItem(
|
|
120
|
+
f.directive, f.description,
|
|
121
|
+
str_(i18n.UIStrings.itemSeverityLow))),
|
|
122
|
+
];
|
|
123
|
+
|
|
124
|
+
return {score: violations.length || syntax.length ? 0 : 1, results};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* @param {LH.Artifacts} artifacts
|
|
129
|
+
* @param {LH.Audit.Context} context
|
|
130
|
+
* @return {Promise<LH.Audit.Product>}
|
|
131
|
+
*/
|
|
132
|
+
static async audit(artifacts, context) {
|
|
133
|
+
const coopHeaders = await this.getRawCoop(artifacts, context);
|
|
134
|
+
const {score, results} = this.constructResults(coopHeaders);
|
|
135
|
+
|
|
136
|
+
/** @type {LH.Audit.Details.Table['headings']} */
|
|
137
|
+
const headings = [
|
|
138
|
+
/* eslint-disable max-len */
|
|
139
|
+
{key: 'description', valueType: 'text', subItemsHeading: {key: 'description'}, label: str_(i18n.UIStrings.columnDescription)},
|
|
140
|
+
{key: 'directive', valueType: 'code', subItemsHeading: {key: 'directive'}, label: str_(UIStrings.columnDirective)},
|
|
141
|
+
{key: 'severity', valueType: 'text', subItemsHeading: {key: 'severity'}, label: str_(UIStrings.columnSeverity)},
|
|
142
|
+
/* eslint-enable max-len */
|
|
143
|
+
];
|
|
144
|
+
const details = Audit.makeTableDetails(headings, results);
|
|
145
|
+
|
|
146
|
+
return {
|
|
147
|
+
score,
|
|
148
|
+
notApplicable: !results.length,
|
|
149
|
+
details,
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export default OriginIsolation;
|
|
155
|
+
export {UIStrings};
|
|
@@ -160,13 +160,13 @@ class CumulativeLayoutShift {
|
|
|
160
160
|
Screenshots: TraceEngine.TraceHandlers.Screenshots,
|
|
161
161
|
});
|
|
162
162
|
// eslint-disable-next-line max-len
|
|
163
|
-
await processor.parse(/** @type {import('@paulirish/trace_engine').Types.
|
|
163
|
+
await processor.parse(/** @type {import('@paulirish/trace_engine').Types.Events.Event[]} */ (
|
|
164
164
|
events
|
|
165
|
-
));
|
|
166
|
-
if (!processor.
|
|
165
|
+
), {});
|
|
166
|
+
if (!processor.parsedTrace) {
|
|
167
167
|
throw new Error('null trace engine result');
|
|
168
168
|
}
|
|
169
|
-
return processor.
|
|
169
|
+
return processor.parsedTrace.LayoutShifts.sessionMaxScore;
|
|
170
170
|
};
|
|
171
171
|
const cumulativeLayoutShift = await run(allFrameShiftEvents.map(e => e.event));
|
|
172
172
|
const cumulativeLayoutShiftMainFrame = await run(mainFrameShiftEvents.map(e => e.event));
|
|
@@ -38,8 +38,10 @@ async function getComputationDataParamsFromTrace(data, context) {
|
|
|
38
38
|
|
|
39
39
|
const graph = await PageDependencyGraph.request({...data, fromTrace: true}, context);
|
|
40
40
|
const traceEngineResult = await TraceEngineResult.request(data, context);
|
|
41
|
-
const
|
|
42
|
-
|
|
41
|
+
const frameId = traceEngineResult.data.Meta.mainFrameId;
|
|
42
|
+
const navigationId = traceEngineResult.data.Meta.mainFrameNavigations[0].args.data.navigationId;
|
|
43
|
+
const processedNavigation = Lantern.TraceEngineComputationData.createProcessedNavigation(
|
|
44
|
+
traceEngineResult.data, frameId, navigationId);
|
|
43
45
|
const simulator = data.simulator || (await LoadSimulator.request(data, context));
|
|
44
46
|
|
|
45
47
|
return {simulator, graph, processedNavigation};
|
|
@@ -11,6 +11,6 @@ declare class NavigationInsights {
|
|
|
11
11
|
* @param {LH.Trace} trace
|
|
12
12
|
* @param {LH.Artifacts.ComputedContext} context
|
|
13
13
|
*/
|
|
14
|
-
static compute_(trace: LH.Trace, context: LH.Artifacts.ComputedContext): Promise<import("@paulirish/trace_engine/models/trace/insights/types.js").
|
|
14
|
+
static compute_(trace: LH.Trace, context: LH.Artifacts.ComputedContext): Promise<import("@paulirish/trace_engine/models/trace/insights/types.js").InsightSet>;
|
|
15
15
|
}
|
|
16
16
|
//# sourceMappingURL=navigation-insights.d.ts.map
|
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
* SPDX-License-Identifier: Apache-2.0
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
+
import log from 'lighthouse-logger';
|
|
8
|
+
|
|
7
9
|
import * as Lantern from '../lib/lantern/lantern.js';
|
|
8
10
|
import {makeComputedArtifact} from './computed-artifact.js';
|
|
9
11
|
import {NetworkRecords} from './network-records.js';
|
|
@@ -16,7 +18,17 @@ class NetworkAnalysis {
|
|
|
16
18
|
*/
|
|
17
19
|
static async compute_(devtoolsLog, context) {
|
|
18
20
|
const records = await NetworkRecords.request(devtoolsLog, context);
|
|
19
|
-
|
|
21
|
+
const analysis = Lantern.Core.NetworkAnalyzer.analyze(records);
|
|
22
|
+
if (!analysis) {
|
|
23
|
+
log.error('NetworkAnalysis', 'Network analysis failed due to lack of transfer data');
|
|
24
|
+
return {
|
|
25
|
+
throughput: 0,
|
|
26
|
+
rtt: Number.POSITIVE_INFINITY,
|
|
27
|
+
additionalRttByOrigin: new Map(),
|
|
28
|
+
serverResponseTimeByOrigin: new Map(),
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
return analysis;
|
|
20
32
|
}
|
|
21
33
|
}
|
|
22
34
|
|
|
@@ -13,6 +13,10 @@ declare class TraceEngineResult {
|
|
|
13
13
|
* @return {Promise<LH.Artifacts.TraceEngineResult>}
|
|
14
14
|
*/
|
|
15
15
|
static runTraceEngine(traceEvents: LH.TraceEvent[]): Promise<LH.Artifacts.TraceEngineResult>;
|
|
16
|
+
/**
|
|
17
|
+
* @param {import('@paulirish/trace_engine/models/trace/insights/types.js').TraceInsightSets} insightSets
|
|
18
|
+
*/
|
|
19
|
+
static localizeInsights(insightSets: import("@paulirish/trace_engine/models/trace/insights/types.js").TraceInsightSets): void;
|
|
16
20
|
/**
|
|
17
21
|
* @param {{trace: LH.Trace}} data
|
|
18
22
|
* @param {LH.Artifacts.ComputedContext} context
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
* SPDX-License-Identifier: Apache-2.0
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
+
import * as i18n from '../lib/i18n/i18n.js';
|
|
7
8
|
import * as TraceEngine from '../lib/trace-engine.js';
|
|
8
9
|
import {makeComputedArtifact} from './computed-artifact.js';
|
|
9
10
|
import {CumulativeLayoutShift} from './metrics/cumulative-layout-shift.js';
|
|
@@ -30,12 +31,37 @@ class TraceEngineResult {
|
|
|
30
31
|
const processor = new TraceEngine.TraceProcessor(traceHandlers);
|
|
31
32
|
|
|
32
33
|
// eslint-disable-next-line max-len
|
|
33
|
-
await processor.parse(/** @type {import('@paulirish/trace_engine').Types.
|
|
34
|
+
await processor.parse(/** @type {import('@paulirish/trace_engine').Types.Events.Event[]} */ (
|
|
34
35
|
traceEvents
|
|
35
|
-
));
|
|
36
|
-
if (!processor.
|
|
36
|
+
), {});
|
|
37
|
+
if (!processor.parsedTrace) throw new Error('No data');
|
|
37
38
|
if (!processor.insights) throw new Error('No insights');
|
|
38
|
-
|
|
39
|
+
this.localizeInsights(processor.insights);
|
|
40
|
+
return {data: processor.parsedTrace, insights: processor.insights};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* @param {import('@paulirish/trace_engine/models/trace/insights/types.js').TraceInsightSets} insightSets
|
|
45
|
+
*/
|
|
46
|
+
static localizeInsights(insightSets) {
|
|
47
|
+
for (const insightSet of insightSets.values()) {
|
|
48
|
+
for (const [name, model] of Object.entries(insightSet.model)) {
|
|
49
|
+
if (model instanceof Error) {
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const key = `node_modules/@paulirish/trace_engine/models/trace/insights/${name}.js`;
|
|
54
|
+
const str_ = i18n.createIcuMessageFn(key, {
|
|
55
|
+
title: model.title,
|
|
56
|
+
description: model.description,
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
// @ts-expect-error coerce to string, should be fine
|
|
60
|
+
model.title = str_(model.title);
|
|
61
|
+
// @ts-expect-error coerce to string, should be fine
|
|
62
|
+
model.description = str_(model.description);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
39
65
|
}
|
|
40
66
|
|
|
41
67
|
/**
|
|
@@ -192,6 +192,8 @@ const defaultConfig = {
|
|
|
192
192
|
'valid-source-maps',
|
|
193
193
|
'prioritize-lcp-image',
|
|
194
194
|
'csp-xss',
|
|
195
|
+
'has-hsts',
|
|
196
|
+
'origin-isolation',
|
|
195
197
|
'script-treemap-data',
|
|
196
198
|
'accessibility/accesskeys',
|
|
197
199
|
'accessibility/aria-allowed-attr',
|
|
@@ -541,6 +543,8 @@ const defaultConfig = {
|
|
|
541
543
|
{id: 'geolocation-on-start', weight: 1, group: 'best-practices-trust-safety'},
|
|
542
544
|
{id: 'notification-on-start', weight: 1, group: 'best-practices-trust-safety'},
|
|
543
545
|
{id: 'csp-xss', weight: 0, group: 'best-practices-trust-safety'},
|
|
546
|
+
{id: 'has-hsts', weight: 0, group: 'best-practices-trust-safety'},
|
|
547
|
+
{id: 'origin-isolation', weight: 0, group: 'best-practices-trust-safety'},
|
|
544
548
|
// User Experience
|
|
545
549
|
{id: 'paste-preventing-inputs', weight: 3, group: 'best-practices-ux'},
|
|
546
550
|
{id: 'image-aspect-ratio', weight: 1, group: 'best-practices-ux'},
|