@forge/csp 0.0.0-experimental-d18f8dd

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 ADDED
@@ -0,0 +1,247 @@
1
+ # @forge/csp
2
+
3
+ ## 0.0.0-experimental-d18f8dd
4
+
5
+ ### Patch Changes
6
+
7
+ - d18f8dd: Force bump
8
+
9
+ ## 1.7.1
10
+
11
+ ### Patch Changes
12
+
13
+ - 4b41a80: Added egress messaging to install prompts
14
+
15
+ ## 1.7.1-next.0
16
+
17
+ ### Patch Changes
18
+
19
+ - 4b41a80: Added egress messaging to install prompts
20
+
21
+ ## 1.7.0
22
+
23
+ ### Minor Changes
24
+
25
+ - ef00257: Add \*.jira.com to allowed host site list
26
+
27
+ ### Patch Changes
28
+
29
+ - d7a1fe3: Update dependencies to remove any transitive dependencies on request
30
+
31
+ ## 1.7.0-next.1
32
+
33
+ ### Patch Changes
34
+
35
+ - d7a1fe3: Update dependencies to remove any transitive dependencies on request
36
+
37
+ ## 1.7.0-next.0
38
+
39
+ ### Minor Changes
40
+
41
+ - ef00257: Add \*.jira.com to allowed host site list
42
+
43
+ ## 1.6.0
44
+
45
+ ### Minor Changes
46
+
47
+ - 8714f5a: Add support for fonts and frames as part of Egress Permissions for Custom UI apps
48
+
49
+ ### Patch Changes
50
+
51
+ - f8ae8a2: Add support for Bitbucket origin in Custom UI
52
+
53
+ ## 1.6.0-next.1
54
+
55
+ ### Patch Changes
56
+
57
+ - f8ae8a2: Add support for Bitbucket origin in Custom UI
58
+
59
+ ## 1.6.0-next.0
60
+
61
+ ### Minor Changes
62
+
63
+ - 8714f5a: Add support for fonts and frames as part of Egress Permissions for Custom UI apps
64
+
65
+ ## 1.5.0
66
+
67
+ ### Minor Changes
68
+
69
+ - 638194f: Fix logic to detect missing fetch egress permission
70
+
71
+ ## 1.5.0-next.0
72
+
73
+ ### Minor Changes
74
+
75
+ - 638194f: Fix logic to detect missing fetch egress permission
76
+
77
+ ## 1.4.0
78
+
79
+ ### Minor Changes
80
+
81
+ - 05f608f: Added external fetch linting
82
+
83
+ ### Patch Changes
84
+
85
+ - bd9194a: Added error protection to egress filtering for URLs with no protocol
86
+
87
+ ## 1.4.0-next.1
88
+
89
+ ### Patch Changes
90
+
91
+ - bd9194a: Added error protection to egress filtering for URLs with no protocol
92
+
93
+ ## 1.4.0-next.0
94
+
95
+ ### Minor Changes
96
+
97
+ - 05f608f: Added external fetch linting
98
+
99
+ ## 1.3.0
100
+
101
+ ### Minor Changes
102
+
103
+ - 9ec2911: Allow style-src as part of Egress Permissions for Custom UI apps
104
+
105
+ ### Patch Changes
106
+
107
+ - 2ddcdb2: Update frame-ancestors for dev
108
+ - 2b3c55d: Fix to restrict frame ancestors of Custom UI apps
109
+
110
+ ## 1.3.0-next.2
111
+
112
+ ### Patch Changes
113
+
114
+ - 2ddcdb2: Update frame-ancestors for dev
115
+
116
+ ## 1.3.0-next.1
117
+
118
+ ### Minor Changes
119
+
120
+ - 9ec2911: Allow style-src as part of Egress Permissions for Custom UI apps
121
+
122
+ ## 1.2.1-next.0
123
+
124
+ ### Patch Changes
125
+
126
+ - 2b3c55d: Fix to restrict frame ancestors of Custom UI apps
127
+
128
+ ## 1.2.0
129
+
130
+ ### Minor Changes
131
+
132
+ - 6c482ef: Add `allow-downloads allow-modals` to sandbox
133
+
134
+ ## 1.2.0-next.0
135
+
136
+ ### Minor Changes
137
+
138
+ - 6c482ef: Add `allow-downloads allow-modals` to sandbox
139
+
140
+ ## 1.1.0
141
+
142
+ ### Minor Changes
143
+
144
+ - f478087: Added logic handle external egress permissions
145
+ - c3ee9e7: Convert permissions.external to CSP options for Custom UI
146
+
147
+ ### Patch Changes
148
+
149
+ - 74a0279: Allowlist images from Atlassian API inside Custom UI apps
150
+ - f8bb329: Adding on user defined CSP from the manifest
151
+
152
+ ## 1.1.0-next.3
153
+
154
+ ### Minor Changes
155
+
156
+ - c3ee9e7: Convert permissions.external to CSP options for Custom UI
157
+
158
+ ## 1.1.0-next.2
159
+
160
+ ### Minor Changes
161
+
162
+ - f478087: Added logic handle external egress permissions
163
+
164
+ ## 1.0.2-next.1
165
+
166
+ ### Patch Changes
167
+
168
+ - f8bb329: Adding on user defined CSP from the manifest
169
+
170
+ ## 1.0.2-next.0
171
+
172
+ ### Patch Changes
173
+
174
+ - 8ad9442: Allowlist images from Atlassian API inside Custom UI apps
175
+
176
+ ## 1.0.1
177
+
178
+ ### Patch Changes
179
+
180
+ - 4ef25ff: change occurrences of Csp to CSP for consistency
181
+
182
+ ## 1.0.1-next.0
183
+
184
+ ### Patch Changes
185
+
186
+ - 4ef25ff: change occurrences of Csp to CSP for consistency
187
+
188
+ ## 1.0.0
189
+
190
+ ### Major Changes
191
+
192
+ - 1daf2c5: Forge packages to 1.0.0 for upcoming platform GA 🎉
193
+
194
+ ### Patch Changes
195
+
196
+ - ca7a9e1: Add local tunnel Custom UI CSP reported server
197
+
198
+ ## 1.0.0-next.1
199
+
200
+ ### Major Changes
201
+
202
+ - 1daf2c5: Forge is now generally available 🎉
203
+
204
+ ## 0.1.2-next.0
205
+
206
+ ### Patch Changes
207
+
208
+ - 1b3bfe1: Add local tunnel Custom UI CSP reported server
209
+
210
+ ## 0.1.1
211
+
212
+ ### Patch Changes
213
+
214
+ - 69064a4: Add secure.gravatar.com to img-src
215
+
216
+ ## 0.1.1-next.0
217
+
218
+ ### Patch Changes
219
+
220
+ - 69064a4: Add secure.gravatar.com to img-src
221
+
222
+ ## 0.1.0
223
+
224
+ ### Minor Changes
225
+
226
+ - 41dcd69: Moved CSP logic to its own package and bridge script to cli-shared
227
+
228
+ ### Patch Changes
229
+
230
+ - a7f8517: Move cli-shared out of the main deps of the CDN
231
+
232
+ ## 0.1.0-next.1
233
+
234
+ ### Patch Changes
235
+
236
+ - a7f8517: Move cli-shared out of the main deps of the CDN
237
+
238
+ ## 0.1.0-next.0
239
+
240
+ ### Minor Changes
241
+
242
+ - 41dcd69: Moved CSP logic to its own package and bridge script to cli-shared
243
+
244
+ ### Patch Changes
245
+
246
+ - Updated dependencies [41dcd69]
247
+ - @forge/cli-shared@0.13.0-next.5
@@ -0,0 +1,10 @@
1
+ import type { LambdaEnvironment } from '@forge/cli-shared';
2
+ import { CSPDetails } from '../types';
3
+ export declare class CSPInjectionService {
4
+ private getCSPReportUri;
5
+ private getForgeGlobalCSP;
6
+ private getExistingCSPDetails;
7
+ private getFrameAncestors;
8
+ getInjectableCSP: (existingCSPDetails: CSPDetails, env: LambdaEnvironment, tunnelCSPReporterUri?: string | undefined) => string[];
9
+ }
10
+ //# sourceMappingURL=csp-injection-service.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"csp-injection-service.d.ts","sourceRoot":"","sources":["../../src/csp/csp-injection-service.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AAE3D,OAAO,EAAE,UAAU,EAAmB,MAAM,UAAU,CAAC;AAUvD,qBAAa,mBAAmB;IAC9B,OAAO,CAAC,eAAe;IAQvB,OAAO,CAAC,iBAAiB;IAIzB,OAAO,CAAC,qBAAqB;IAI7B,OAAO,CAAC,iBAAiB;IAYlB,gBAAgB,uBACD,UAAU,OACzB,iBAAiB,gDAErB,MAAM,EAAE,CA4CT;CACH"}
@@ -0,0 +1,82 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.CSPInjectionService = void 0;
4
+ const types_1 = require("../types");
5
+ const atlassianImageHosts = {
6
+ dev: ['https://avatar-management--avatars.us-west-2.staging.public.atl-paas.net', 'https://api.dev.atlassian.com'],
7
+ stg: ['https://avatar-management--avatars.us-west-2.staging.public.atl-paas.net', 'https://api.stg.atlassian.com'],
8
+ prod: ['https://avatar-management--avatars.us-west-2.prod.public.atl-paas.net', 'https://api.atlassian.com']
9
+ };
10
+ const gravatarUrl = 'https://secure.gravatar.com';
11
+ class CSPInjectionService {
12
+ constructor() {
13
+ this.getInjectableCSP = (existingCSPDetails, env, tunnelCSPReporterUri) => {
14
+ const reportUri = tunnelCSPReporterUri || this.getCSPReportUri(env);
15
+ const defaultSrc = `'self'`;
16
+ const frameAncestors = ["'self'", ...this.getFrameAncestors(env)].join(' ');
17
+ const frameSrc = ["'self'", ...this.getExistingCSPDetails(types_1.ExternalCspType.FRAME_SRC, existingCSPDetails)].join(' ');
18
+ const fontSrc = ["'self'", ...this.getExistingCSPDetails(types_1.ExternalCspType.FONT_SRC, existingCSPDetails)].join(' ');
19
+ const imgSrc = [
20
+ "'self'",
21
+ 'data:',
22
+ 'blob:',
23
+ gravatarUrl,
24
+ ...atlassianImageHosts[env],
25
+ ...this.getExistingCSPDetails(types_1.ExternalCspType.IMG_SRC, existingCSPDetails)
26
+ ].join(' ');
27
+ const mediaSrc = [
28
+ "'self'",
29
+ 'data:',
30
+ 'blob:',
31
+ ...this.getExistingCSPDetails(types_1.ExternalCspType.MEDIA_SRC, existingCSPDetails)
32
+ ].join(' ');
33
+ const connectSrc = ["'self'", ...this.getExistingCSPDetails(types_1.ExternalCspType.CONNECT_SRC, existingCSPDetails)].join(' ');
34
+ const scriptSrc = [
35
+ "'self'",
36
+ this.getForgeGlobalCSP(env),
37
+ ...this.getExistingCSPDetails(types_1.ExternalCspType.SCRIPT_SRC, existingCSPDetails)
38
+ ].join(' ');
39
+ const styleSrc = ["'self'", ...this.getExistingCSPDetails(types_1.ExternalCspType.STYLE_SRC, existingCSPDetails)].join(' ');
40
+ return [
41
+ `default-src ${defaultSrc}`,
42
+ `frame-ancestors ${frameAncestors}`,
43
+ `frame-src ${frameSrc}`,
44
+ `font-src ${fontSrc}`,
45
+ `img-src ${imgSrc}`,
46
+ `media-src ${mediaSrc}`,
47
+ `connect-src ${connectSrc}`,
48
+ `script-src ${scriptSrc}`,
49
+ `style-src ${styleSrc}`,
50
+ `form-action 'self'`,
51
+ `sandbox allow-downloads allow-forms allow-modals allow-same-origin allow-scripts`,
52
+ `report-uri ${reportUri}`
53
+ ];
54
+ };
55
+ }
56
+ getCSPReportUri(env, tunnelCSPReporterUri) {
57
+ if (tunnelCSPReporterUri)
58
+ return tunnelCSPReporterUri;
59
+ if (env === 'prod')
60
+ return 'https://web-security-reports.services.atlassian.com/csp-report/forge-cdn';
61
+ return 'https://web-security-reports.stg.services.atlassian.com/csp-report/forge-cdn';
62
+ }
63
+ getForgeGlobalCSP(env) {
64
+ return `https://forge.cdn.${env}.atlassian-dev.net`;
65
+ }
66
+ getExistingCSPDetails(cspType, cspDetails) {
67
+ var _a;
68
+ return (_a = cspDetails[cspType]) !== null && _a !== void 0 ? _a : [];
69
+ }
70
+ getFrameAncestors(env) {
71
+ if (env === 'prod')
72
+ return ['*.atlassian.net', 'bitbucket.org', '*.jira.com'];
73
+ return [
74
+ '*.jira-dev.com',
75
+ 'http://localhost:*',
76
+ '*.devbucket.org',
77
+ 'https://staging.bb-inf.net',
78
+ 'https://integration.bb-inf.net'
79
+ ];
80
+ }
81
+ }
82
+ exports.CSPInjectionService = CSPInjectionService;
@@ -0,0 +1,21 @@
1
+ import type { Logger } from '@forge/cli-shared';
2
+ import type { Permissions } from '@forge/manifest';
3
+ import { ContentPermissions, CSPDetails, DocumentBody } from '../types';
4
+ export declare class CSPProcessingService {
5
+ private readonly logger;
6
+ constructor(logger: Pick<Logger, 'info'>);
7
+ getCspDetails(body: DocumentBody, permissions: Permissions): CSPDetails;
8
+ getInvalidCspPermissions(contentPermissions: ContentPermissions): string[];
9
+ private mapExternalPermissionsToCsp;
10
+ private getStyleSrc;
11
+ private getScriptSrc;
12
+ private extractUniqueHashes;
13
+ private getInlineScriptHashes;
14
+ private hashScript;
15
+ private isValidUserScriptSrc;
16
+ private isValidUserStyleSrc;
17
+ private isSafeCsp;
18
+ private isValidHash;
19
+ private getDeprecatedUserCsp;
20
+ }
21
+ //# sourceMappingURL=csp-processing-service.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"csp-processing-service.d.ts","sourceRoot":"","sources":["../../src/csp/csp-processing-service.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,mBAAmB,CAAC;AAChD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAKnD,OAAO,EAAE,kBAAkB,EAAE,UAAU,EAAE,YAAY,EAAuB,MAAM,UAAU,CAAC;AAiB7F,qBAAa,oBAAoB;IACnB,OAAO,CAAC,QAAQ,CAAC,MAAM;gBAAN,MAAM,EAAE,IAAI,CAAC,MAAM,EAAE,MAAM,CAAC;IAElD,aAAa,CAAC,IAAI,EAAE,YAAY,EAAE,WAAW,EAAE,WAAW,GAAG,UAAU;IAkBvE,wBAAwB,CAAC,kBAAkB,EAAE,kBAAkB,GAAG,MAAM,EAAE;IASjF,OAAO,CAAC,2BAA2B;IAcnC,OAAO,CAAC,WAAW;IASnB,OAAO,CAAC,YAAY;IAWpB,OAAO,CAAC,mBAAmB;IAoB3B,OAAO,CAAC,qBAAqB;IAW7B,OAAO,CAAC,UAAU;IAKlB,OAAO,CAAC,oBAAoB;IAM5B,OAAO,CAAC,mBAAmB;IAW3B,OAAO,CAAC,SAAS;IAIjB,OAAO,CAAC,WAAW;IAKnB,OAAO,CAAC,oBAAoB;CAa7B"}
@@ -0,0 +1,117 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.CSPProcessingService = void 0;
4
+ const tslib_1 = require("tslib");
5
+ const cheerio_1 = tslib_1.__importDefault(require("cheerio"));
6
+ const content_security_policy_parser_1 = tslib_1.__importDefault(require("content-security-policy-parser"));
7
+ const crypto_1 = tslib_1.__importDefault(require("crypto"));
8
+ const STYLE_SRC_ALLOWLIST = [`'unsafe-inline'`];
9
+ const SCRIPT_SRC_ALLOWLIST = [`'unsafe-inline'`, `'unsafe-eval'`, `'unsafe-hashes'`];
10
+ const BASE_64_HASH_PATTERNS = [
11
+ /^'sha256-[a-zA-Z0-9=+/]{44}'$/,
12
+ /^'sha384-[a-zA-Z0-9=+/]{64}'$/,
13
+ /^'sha512-[a-zA-Z0-9=+/]{88}'$/
14
+ ];
15
+ class CSPProcessingService {
16
+ constructor(logger) {
17
+ this.logger = logger;
18
+ }
19
+ getCspDetails(body, permissions) {
20
+ var _a, _b;
21
+ const { scripts, styles } = (_a = permissions === null || permissions === void 0 ? void 0 : permissions.content) !== null && _a !== void 0 ? _a : { scripts: [], styles: [] };
22
+ const external = (_b = permissions === null || permissions === void 0 ? void 0 : permissions.external) !== null && _b !== void 0 ? _b : {};
23
+ const $ = cheerio_1.default.load(body);
24
+ const _c = this.mapExternalPermissionsToCsp(external), { 'script-src': scriptSrc, 'style-src': styleSrc } = _c, mappedExternalCsp = tslib_1.__rest(_c, ['script-src', 'style-src']);
25
+ return Object.assign({ 'style-src': [...this.getStyleSrc($, styles), ...styleSrc], 'script-src': [...this.getScriptSrc($, scripts), ...scriptSrc] }, mappedExternalCsp);
26
+ }
27
+ getInvalidCspPermissions(contentPermissions) {
28
+ var _a, _b;
29
+ const { styles, scripts } = contentPermissions;
30
+ const invalidStyles = (_a = styles === null || styles === void 0 ? void 0 : styles.filter((styleSrc) => !this.isValidUserStyleSrc(`'${styleSrc}'`))) !== null && _a !== void 0 ? _a : [];
31
+ const invalidScripts = (_b = scripts === null || scripts === void 0 ? void 0 : scripts.filter((scriptSrc) => !this.isValidUserScriptSrc(`'${scriptSrc}'`))) !== null && _b !== void 0 ? _b : [];
32
+ return [...invalidStyles, ...invalidScripts];
33
+ }
34
+ mapExternalPermissionsToCsp(externalPermissions) {
35
+ var _a;
36
+ const { images, media, scripts, fetch, styles, fonts, frames } = externalPermissions;
37
+ return {
38
+ 'img-src': images !== null && images !== void 0 ? images : [],
39
+ 'media-src': media !== null && media !== void 0 ? media : [],
40
+ 'script-src': scripts !== null && scripts !== void 0 ? scripts : [],
41
+ 'style-src': styles !== null && styles !== void 0 ? styles : [],
42
+ 'connect-src': (_a = fetch === null || fetch === void 0 ? void 0 : fetch.client) !== null && _a !== void 0 ? _a : [],
43
+ 'font-src': fonts !== null && fonts !== void 0 ? fonts : [],
44
+ 'frame-src': frames !== null && frames !== void 0 ? frames : []
45
+ };
46
+ }
47
+ getStyleSrc($, userStyleSrc) {
48
+ var _a, _b;
49
+ const quotedUserStyleSrc = (_a = userStyleSrc === null || userStyleSrc === void 0 ? void 0 : userStyleSrc.map((x) => `'${x}'`)) !== null && _a !== void 0 ? _a : [];
50
+ const deprecatedUserScriptSrc = (_b = this.getDeprecatedUserCsp($)['style-src']) !== null && _b !== void 0 ? _b : [];
51
+ const uniqueStyleSrc = [...new Set([...deprecatedUserScriptSrc, ...quotedUserStyleSrc])];
52
+ return uniqueStyleSrc.filter((x) => this.isValidUserStyleSrc(x));
53
+ }
54
+ getScriptSrc($, userScriptSrc) {
55
+ var _a;
56
+ const generatedScriptHashes = this.getInlineScriptHashes($);
57
+ const quotedUserScriptSrc = (_a = userScriptSrc === null || userScriptSrc === void 0 ? void 0 : userScriptSrc.map((x) => `'${x}'`)) !== null && _a !== void 0 ? _a : [];
58
+ const validUserScriptSrc = quotedUserScriptSrc.filter((x) => this.isValidUserScriptSrc(x));
59
+ const { scriptSrc, userScriptHashes } = this.extractUniqueHashes(validUserScriptSrc, generatedScriptHashes);
60
+ return [...scriptSrc, ...generatedScriptHashes, ...userScriptHashes];
61
+ }
62
+ extractUniqueHashes(userScriptSrc, existingScriptHashes) {
63
+ var _a;
64
+ const userScriptHashes = [];
65
+ const scriptSrc = (_a = userScriptSrc === null || userScriptSrc === void 0 ? void 0 : userScriptSrc.filter((scriptSrc) => {
66
+ const isValidHash = this.isValidHash(scriptSrc);
67
+ if (isValidHash && !existingScriptHashes.includes(scriptSrc)) {
68
+ userScriptHashes.push(scriptSrc);
69
+ }
70
+ return !isValidHash;
71
+ })) !== null && _a !== void 0 ? _a : [];
72
+ return { scriptSrc, userScriptHashes };
73
+ }
74
+ getInlineScriptHashes($) {
75
+ const scriptHashes = $('script:not([src])')
76
+ .map((_index, script) => {
77
+ const html = $(script).html();
78
+ return html && `'sha256-${this.hashScript(html)}'`;
79
+ })
80
+ .get();
81
+ return scriptHashes;
82
+ }
83
+ hashScript(content) {
84
+ const sha256 = crypto_1.default.createHash('sha256');
85
+ return sha256.update(content).digest('base64');
86
+ }
87
+ isValidUserScriptSrc(scriptSrc) {
88
+ if (!this.isSafeCsp(scriptSrc))
89
+ return false;
90
+ return this.isValidHash(scriptSrc) || SCRIPT_SRC_ALLOWLIST.includes(scriptSrc);
91
+ }
92
+ isValidUserStyleSrc(styleSrc) {
93
+ if (!this.isSafeCsp(styleSrc)) {
94
+ this.logger.info('discarding potentially-malicious CSP');
95
+ return false;
96
+ }
97
+ return STYLE_SRC_ALLOWLIST.includes(styleSrc);
98
+ }
99
+ isSafeCsp(cspString) {
100
+ return /^[a-zA-Z0-9='"+\/ -]*$/.test(cspString);
101
+ }
102
+ isValidHash(cspString) {
103
+ return BASE_64_HASH_PATTERNS.some((pattern) => pattern.test(cspString));
104
+ }
105
+ getDeprecatedUserCsp($) {
106
+ const cspContent = $('meta[http-equiv="Content-Security-Policy"]').attr('content');
107
+ if (!cspContent) {
108
+ return {};
109
+ }
110
+ if (!this.isSafeCsp(cspContent)) {
111
+ this.logger.info('discarding potentially-malicious CSP');
112
+ return {};
113
+ }
114
+ return content_security_policy_parser_1.default(cspContent);
115
+ }
116
+ }
117
+ exports.CSPProcessingService = CSPProcessingService;
@@ -0,0 +1,3 @@
1
+ export * from './csp-injection-service';
2
+ export * from './csp-processing-service';
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/csp/index.ts"],"names":[],"mappings":"AAEA,cAAc,yBAAyB,CAAC;AACxC,cAAc,0BAA0B,CAAC"}
@@ -0,0 +1,5 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const tslib_1 = require("tslib");
4
+ tslib_1.__exportStar(require("./csp-injection-service"), exports);
5
+ tslib_1.__exportStar(require("./csp-processing-service"), exports);
@@ -0,0 +1,11 @@
1
+ export declare class EgressFilteringService {
2
+ private readonly URLs;
3
+ private readonly wildcardDomains;
4
+ private readonly allowsEverything;
5
+ constructor(allowList: string[]);
6
+ private safeURL;
7
+ isValidUrl(url: string): boolean;
8
+ private domainCheck;
9
+ private domainIsAllowed;
10
+ }
11
+ //# sourceMappingURL=egress-filtering-service.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"egress-filtering-service.d.ts","sourceRoot":"","sources":["../../src/egress/egress-filtering-service.ts"],"names":[],"mappings":"AAGA,qBAAa,sBAAsB;IACjC,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAQ;IAC7B,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAQ;IACxC,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAAU;gBAE/B,SAAS,EAAE,MAAM,EAAE;IAY/B,OAAO,CAAC,OAAO;IAOR,UAAU,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO;IAQvC,OAAO,CAAC,WAAW;IAWnB,OAAO,CAAC,eAAe;CAWxB"}
@@ -0,0 +1,46 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.EgressFilteringService = void 0;
4
+ const tslib_1 = require("tslib");
5
+ const micromatch_1 = tslib_1.__importDefault(require("micromatch"));
6
+ const url_1 = require("url");
7
+ class EgressFilteringService {
8
+ constructor(allowList) {
9
+ this.URLs = allowList
10
+ .filter((domainOrURL) => !domainOrURL.startsWith('*'))
11
+ .map((url) => this.safeURL(url));
12
+ this.wildcardDomains = allowList
13
+ .filter((domainOrURL) => domainOrURL !== '*')
14
+ .map((url) => this.safeURL(url))
15
+ .filter((url) => url.hostname.startsWith('*'));
16
+ this.allowsEverything = allowList.includes('*');
17
+ }
18
+ safeURL(url, defaultProtocol = 'https://') {
19
+ const protocolRegex = /^(.*:\/\/)/;
20
+ return new url_1.URL(protocolRegex.test(url) ? url : `${defaultProtocol}${url}`);
21
+ }
22
+ isValidUrl(url) {
23
+ if (this.allowsEverything) {
24
+ return true;
25
+ }
26
+ return this.domainIsAllowed(this.safeURL(url));
27
+ }
28
+ domainCheck(domain, allowList) {
29
+ const hostnameMatchedProtocol = allowList
30
+ .filter((allowed) => allowed.protocol === domain.protocol)
31
+ .map((url) => url.hostname);
32
+ return (micromatch_1.default([domain.hostname], hostnameMatchedProtocol, {
33
+ dot: true
34
+ }).length > 0);
35
+ }
36
+ domainIsAllowed(domain) {
37
+ if (this.domainCheck(domain, this.URLs)) {
38
+ return true;
39
+ }
40
+ if (this.domainCheck(domain, this.wildcardDomains)) {
41
+ return true;
42
+ }
43
+ return false;
44
+ }
45
+ }
46
+ exports.EgressFilteringService = EgressFilteringService;
@@ -0,0 +1,3 @@
1
+ export * from './egress-filtering-service';
2
+ export * from './utils';
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/egress/index.ts"],"names":[],"mappings":"AAEA,cAAc,4BAA4B,CAAC;AAC3C,cAAc,SAAS,CAAC"}
@@ -0,0 +1,5 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const tslib_1 = require("tslib");
4
+ tslib_1.__exportStar(require("./egress-filtering-service"), exports);
5
+ tslib_1.__exportStar(require("./utils"), exports);
@@ -0,0 +1,3 @@
1
+ declare const sortAndGroupEgressPermissionsByDomain: (egressAddresses: string[]) => Array<string>;
2
+ export { sortAndGroupEgressPermissionsByDomain };
3
+ //# sourceMappingURL=utils.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../../src/egress/utils.ts"],"names":[],"mappings":"AAGA,QAAA,MAAM,qCAAqC,oBAAqB,MAAM,EAAE,KAAG,KAAK,CAAC,MAAM,CA2BtF,CAAC;AAEF,OAAO,EAAE,qCAAqC,EAAE,CAAC"}
@@ -0,0 +1,31 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.sortAndGroupEgressPermissionsByDomain = void 0;
4
+ const tslib_1 = require("tslib");
5
+ const micromatch_1 = tslib_1.__importDefault(require("micromatch"));
6
+ const url_1 = require("url");
7
+ const sortAndGroupEgressPermissionsByDomain = (egressAddresses) => {
8
+ const protocolRegex = /^(.*?:\/\/)/;
9
+ const domainSet = new Set();
10
+ const groupSet = new Set();
11
+ const removeSet = new Set();
12
+ if ((egressAddresses === null || egressAddresses === void 0 ? void 0 : egressAddresses.length) === 0) {
13
+ return [];
14
+ }
15
+ egressAddresses.forEach((item) => {
16
+ const itemWithProtocol = protocolRegex.test(item) ? item : `https://${item}`;
17
+ const url = new url_1.URL(itemWithProtocol);
18
+ if (url.hostname.startsWith('*')) {
19
+ groupSet.add(url.hostname.substring(2));
20
+ removeSet.add('!' + url.hostname);
21
+ }
22
+ else {
23
+ domainSet.add(url.hostname);
24
+ }
25
+ });
26
+ if (removeSet.size === 0) {
27
+ return [...domainSet];
28
+ }
29
+ return [...new Set(micromatch_1.default([...domainSet], [...removeSet]).concat([...groupSet]))].sort();
30
+ };
31
+ exports.sortAndGroupEgressPermissionsByDomain = sortAndGroupEgressPermissionsByDomain;
package/out/index.d.ts ADDED
@@ -0,0 +1,4 @@
1
+ export * from './csp';
2
+ export * from './egress';
3
+ export * from './types';
4
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAEA,cAAc,OAAO,CAAC;AACtB,cAAc,UAAU,CAAC;AACzB,cAAc,SAAS,CAAC"}
package/out/index.js ADDED
@@ -0,0 +1,6 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const tslib_1 = require("tslib");
4
+ tslib_1.__exportStar(require("./csp"), exports);
5
+ tslib_1.__exportStar(require("./egress"), exports);
6
+ tslib_1.__exportStar(require("./types"), exports);
package/out/types.d.ts ADDED
@@ -0,0 +1,16 @@
1
+ /// <reference types="node" />
2
+ import type { Permissions } from '@forge/manifest';
3
+ export declare type DocumentBody = string | Buffer;
4
+ export declare type ContentPermissions = NonNullable<Permissions['content']>;
5
+ export declare type ExternalPermissions = NonNullable<Permissions['external']>;
6
+ export declare enum ExternalCspType {
7
+ IMG_SRC = "img-src",
8
+ MEDIA_SRC = "media-src",
9
+ SCRIPT_SRC = "script-src",
10
+ STYLE_SRC = "style-src",
11
+ CONNECT_SRC = "connect-src",
12
+ FONT_SRC = "font-src",
13
+ FRAME_SRC = "frame-src"
14
+ }
15
+ export declare type CSPDetails = Record<ExternalCspType, string[]>;
16
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":";AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAEnD,oBAAY,YAAY,GAAG,MAAM,GAAG,MAAM,CAAC;AAE3C,oBAAY,kBAAkB,GAAG,WAAW,CAAC,WAAW,CAAC,SAAS,CAAC,CAAC,CAAC;AACrE,oBAAY,mBAAmB,GAAG,WAAW,CAAC,WAAW,CAAC,UAAU,CAAC,CAAC,CAAC;AACvE,oBAAY,eAAe;IACzB,OAAO,YAAY;IACnB,SAAS,cAAc;IACvB,UAAU,eAAe;IACzB,SAAS,cAAc;IACvB,WAAW,gBAAgB;IAC3B,QAAQ,aAAa;IACrB,SAAS,cAAc;CACxB;AACD,oBAAY,UAAU,GAAG,MAAM,CAAC,eAAe,EAAE,MAAM,EAAE,CAAC,CAAC"}
package/out/types.js ADDED
@@ -0,0 +1,13 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ExternalCspType = void 0;
4
+ var ExternalCspType;
5
+ (function (ExternalCspType) {
6
+ ExternalCspType["IMG_SRC"] = "img-src";
7
+ ExternalCspType["MEDIA_SRC"] = "media-src";
8
+ ExternalCspType["SCRIPT_SRC"] = "script-src";
9
+ ExternalCspType["STYLE_SRC"] = "style-src";
10
+ ExternalCspType["CONNECT_SRC"] = "connect-src";
11
+ ExternalCspType["FONT_SRC"] = "font-src";
12
+ ExternalCspType["FRAME_SRC"] = "frame-src";
13
+ })(ExternalCspType = exports.ExternalCspType || (exports.ExternalCspType = {}));
package/package.json ADDED
@@ -0,0 +1,23 @@
1
+ {
2
+ "name": "@forge/csp",
3
+ "version": "0.0.0-experimental-d18f8dd",
4
+ "description": "Contains the CSP configuration for Custom UI resources in Forge",
5
+ "main": "out/index.js",
6
+ "author": "Atlassian",
7
+ "license": "UNLICENSED",
8
+ "scripts": {
9
+ "build": "yarn run clean && yarn run compile",
10
+ "compile": "tsc -b -v",
11
+ "clean": "rm -rf ./out && rm -f tsconfig.tsbuildinfo"
12
+ },
13
+ "devDependencies": {
14
+ "@forge/cli-shared": "^0.0.0-experimental-d18f8dd",
15
+ "@forge/manifest": "^0.0.0-experimental-d18f8dd",
16
+ "@types/jest": "^26.0.0"
17
+ },
18
+ "dependencies": {
19
+ "cheerio": "^0.22.0",
20
+ "content-security-policy-parser": "^0.3.0",
21
+ "micromatch": "^4.0.2"
22
+ }
23
+ }