eslint-plugin-greasemonkey 1.9.0 → 1.10.1
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/README.md +481 -13
- package/package.json +12 -8
- package/src/data/apis.js +56 -53
- package/src/data/headers.js +45 -35
- package/src/data/regex.js +14 -0
- package/src/index.d.ts +1 -0
- package/src/rules/meta-order.js +2 -2
- package/src/rules/meta-spacing.js +9 -8
- package/src/rules/no-insecure-fetch.js +24 -25
- package/src/rules/no-insecure-messaging.js +2 -3
- package/src/rules/no-invalid-grants.js +10 -8
- package/src/rules/no-invalid-headers.js +9 -9
- package/src/rules/no-missing-grants.js +4 -6
- package/src/rules/no-missing-resources.js +6 -5
- package/src/rules/no-sensitive-grants.js +81 -0
- package/src/rules/no-unused-resources.js +6 -5
- package/src/rules/prefer-gm-add-style.js +12 -11
- package/src/rules/require-license-header.js +27 -0
- package/src/rules/require-space-before-meta-key.js +2 -3
- package/src/rules/valid-field-values.js +5 -5
- package/src/rules/valid-icon-headers.js +74 -0
- package/src/rules/valid-license-header.js +73 -0
- package/src/rules/valid-url-headers.js +114 -0
- package/src/rules/valid-version-header.js +83 -0
- package/src/utils/api.js +11 -0
- package/src/utils/array.js +1 -0
- package/src/utils/deprecations.js +2 -2
- package/src/utils/init.js +23 -8
- package/src/utils/license.js +56 -0
- package/src/utils/metadata.js +2 -2
- package/src/utils/pkg.js +21 -0
- package/src/utils/template.js +1 -1
package/src/data/apis.js
CHANGED
|
@@ -1,54 +1,57 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
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
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
1
|
+
const apis = {
|
|
2
|
+
ag: new Set([
|
|
3
|
+
'GM_info', 'GM.info', 'GM_setValue', 'GM.setValue', 'GM_getValue', 'GM.getValue',
|
|
4
|
+
'GM_listValues', 'GM.listValues', 'GM_deleteValue', 'GM.deleteValue',
|
|
5
|
+
'GM_getResourceURL', 'GM.getResourceUrl', 'GM_setClipboard', 'GM.setClipboard',
|
|
6
|
+
'GM_xmlhttpRequest', 'GM.xmlHttpRequest', 'GM_openInTab', 'GM.openInTab',
|
|
7
|
+
'GM_notification', 'unsafeWindow', 'GM_getResourceText', 'GM_addStyle', 'GM_log',
|
|
8
|
+
'GM_addElement', 'GM.addElement'
|
|
9
|
+
]),
|
|
10
|
+
gm: {
|
|
11
|
+
legacy: new Set([
|
|
12
|
+
'GM_addElement', 'GM_addStyle', 'GM_addValueChangeListener', 'GM_deleteValue', 'GM_download',
|
|
13
|
+
'GM_getResourceText', 'GM_getResourceURL', 'GM_getTab', 'GM_getTabs', 'GM_getValue', 'GM_getValues',
|
|
14
|
+
'GM_info', 'GM_listValues', 'GM_log', 'GM_notification', 'GM_openInTab', 'GM_registerMenuCommand',
|
|
15
|
+
'GM_removeValueChangeListener', // oxlint-disable-line no-secrets/no-secrets
|
|
16
|
+
'GM_saveTab', 'GM_setClipboard', 'GM_setValue', 'GM_setValues', 'GM_unregisterMenuCommand', 'GM_webRequest',
|
|
17
|
+
'GM_xmlhttpRequest'
|
|
18
|
+
]),
|
|
19
|
+
modern: new Set([
|
|
20
|
+
'GM.addElement', 'GM.addStyle', 'GM.addValueChangeListener', 'GM.deleteValue', 'GM.download',
|
|
21
|
+
'GM.getResourceText', 'GM.getResourceUrl', 'GM.getTab', 'GM.getTabs', 'GM.getValue', 'GM.getValues',
|
|
22
|
+
'GM.info', 'GM.listValues', 'GM.log', 'GM.notification', 'GM.openInTab', 'GM.registerMenuCommand',
|
|
23
|
+
'GM.removeValueChangeListener', 'GM.saveTab', 'GM.setClipboard', 'GM.setValue', 'GM.setValues',
|
|
24
|
+
'GM.unregisterMenuCommand', 'GM.webRequest', 'GM.xmlHttpRequest'
|
|
25
|
+
])
|
|
26
|
+
},
|
|
27
|
+
sc: new Set([
|
|
28
|
+
'CAT.agent.conversation', 'CAT.agent.dom', 'CAT.agent.mcp', 'CAT.agent.model', 'CAT.agent.opfs',
|
|
29
|
+
'CAT.agent.skills', 'CAT.agent.task', 'CAT_fileStorage', 'CAT_scriptLoaded', 'CAT_userConfig'
|
|
30
|
+
]),
|
|
31
|
+
tm: new Set([
|
|
32
|
+
'GM_getTab', 'GM_getTabs', 'GM_saveTab', 'GM_webRequest',
|
|
33
|
+
'GM.getTab', 'GM.getTabs', 'GM.saveTab', 'GM.webRequest'
|
|
34
|
+
]),
|
|
35
|
+
sensitive: new Set([
|
|
36
|
+
'GM_xmlhttpRequest', 'GM.xmlHttpRequest', 'GM_download', 'GM.download',
|
|
37
|
+
'GM_openInTab', 'GM.openInTab', 'GM_setClipboard', 'GM.setClipboard',
|
|
38
|
+
'GM_notification', 'GM.notification', 'GM_addValueChangeListener', 'GM.addValueChangeListener',
|
|
39
|
+
'GM_getResourceURL', 'GM.getResourceUrl', 'unsafeWindow'
|
|
40
|
+
]),
|
|
41
|
+
managerSensitive: new Map([
|
|
42
|
+
['tampermonkey', new Set([
|
|
43
|
+
'GM_webRequest', 'GM.webRequest', 'GM_getTab', 'GM.getTab', 'GM_getTabs', 'GM.getTabs',
|
|
44
|
+
'GM_saveTab', 'GM.saveTab'
|
|
45
|
+
])],
|
|
46
|
+
['violentmonkey', new Set([])],
|
|
47
|
+
['greasemonkey', new Set([])],
|
|
48
|
+
['scriptcat', new Set([
|
|
49
|
+
'CAT.agent.conversation', 'CAT.agent.dom', 'CAT.agent.mcp', 'CAT.agent.model', 'CAT.agent.opfs',
|
|
50
|
+
'CAT.agent.skills', 'CAT.agent.task', 'CAT_fileStorage', 'CAT_userConfig'
|
|
51
|
+
])],
|
|
52
|
+
['adguard', new Set([])]
|
|
53
|
+
])
|
|
54
54
|
}
|
|
55
|
+
apis.all = new Set([...apis.gm.legacy, ...apis.gm.modern, ...apis.sc])
|
|
56
|
+
|
|
57
|
+
export default apis
|
package/src/data/headers.js
CHANGED
|
@@ -1,41 +1,51 @@
|
|
|
1
|
-
|
|
2
|
-
'antifeature', 'author', 'compatible', 'connect', 'contributionAmount', 'contributionURL', 'copyright',
|
|
3
|
-
'defaulticon', 'description', 'downloadURL', 'exclude', 'exclude-match', 'grant', 'homepage', 'homepageURL',
|
|
4
|
-
'icon', 'icon64', 'include', 'license', 'match', 'name', 'namespace', 'noframes', 'require', 'resource',
|
|
5
|
-
'run-at', 'source', 'supportURL', 'updateURL', 'version', 'website'
|
|
6
|
-
])
|
|
1
|
+
// oxlint-disable sonarjs/no-clear-text-protocols
|
|
7
2
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
['
|
|
18
|
-
[
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
]
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
3
|
+
const headers = {
|
|
4
|
+
broadConnect: new Set([
|
|
5
|
+
'*',
|
|
6
|
+
'*.com', '*.org', '*.net', '*.io', '*.dev', '*.app', '*.co',
|
|
7
|
+
'http://*', 'https://*',
|
|
8
|
+
'http://*/*', 'https://*/*',
|
|
9
|
+
'http://*/', 'https://*/',
|
|
10
|
+
'http:*', 'https:*'
|
|
11
|
+
]),
|
|
12
|
+
localized: ['name', 'description', 'antifeature'],
|
|
13
|
+
manager: new Map([
|
|
14
|
+
['unwrap', ['tampermonkey', 'violentmonkey', 'greasemonkey', 'scriptcat']],
|
|
15
|
+
['run-in', ['tampermonkey', 'scriptcat']],
|
|
16
|
+
['nocompat', ['tampermonkey']],
|
|
17
|
+
['sandbox', ['tampermonkey']],
|
|
18
|
+
['user-agent', ['tampermonkey']],
|
|
19
|
+
['webRequest', ['tampermonkey']],
|
|
20
|
+
['inject-into', ['violentmonkey', 'scriptcat']],
|
|
21
|
+
['top-level-await', ['violentmonkey']],
|
|
22
|
+
['background', ['scriptcat']],
|
|
23
|
+
['crontab', ['scriptcat']],
|
|
24
|
+
['definition', ['scriptcat']],
|
|
25
|
+
['early-start', ['scriptcat']],
|
|
26
|
+
['require-css', ['scriptcat']],
|
|
27
|
+
['storageName', ['scriptcat']]
|
|
28
|
+
]),
|
|
29
|
+
managers: ['tampermonkey', 'violentmonkey', 'greasemonkey', 'scriptcat', 'adguard'],
|
|
30
|
+
universal: new Set([
|
|
31
|
+
'antifeature', 'author', 'compatible', 'connect', 'contributionAmount', 'contributionURL', 'copyright',
|
|
32
|
+
'defaulticon', 'description', 'downloadURL', 'exclude', 'exclude-match', 'grant', 'homepage', 'homepageURL',
|
|
33
|
+
'icon', 'icon64', 'include', 'license', 'match', 'name', 'namespace', 'noframes', 'require', 'resource',
|
|
34
|
+
'run-at', 'source', 'supportURL', 'updateURL', 'version', 'website'
|
|
35
|
+
])
|
|
36
|
+
}
|
|
28
37
|
|
|
29
|
-
|
|
38
|
+
const knownHeaders = new Set([...headers.universal, ...headers.manager.keys()])
|
|
30
39
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
40
|
+
headers.valid = new Map([['all', knownHeaders]])
|
|
41
|
+
for (const mgr of headers.managers) {
|
|
42
|
+
const specific = [...headers.manager]
|
|
43
|
+
.filter(([, supportedBy]) => supportedBy.includes(mgr))
|
|
34
44
|
.map(([header]) => header)
|
|
35
|
-
|
|
45
|
+
headers.valid.set(mgr, new Set([...headers.universal, ...specific]))
|
|
36
46
|
}
|
|
37
47
|
|
|
38
|
-
|
|
48
|
+
headers.url = new Map(Object.entries({
|
|
39
49
|
connect: 'host-pattern',
|
|
40
50
|
contributionURL: 'url',
|
|
41
51
|
defaulticon: 'url',
|
|
@@ -55,6 +65,6 @@ const urlHeaderTypes = {
|
|
|
55
65
|
supportURL: 'url',
|
|
56
66
|
updateURL: 'url',
|
|
57
67
|
website: 'url'
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
export
|
|
68
|
+
}).filter(([name]) => knownHeaders.has(name)))
|
|
69
|
+
|
|
70
|
+
export default headers
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export default {
|
|
2
|
+
filename: {
|
|
3
|
+
license: // oxlint-disable-next-line security/detect-unsafe-regex
|
|
4
|
+
/^(?:[\w.-]*-)?(?:LICEN[SC]E|COPYING)(?:[-.][\w.-]+)?$/i
|
|
5
|
+
},
|
|
6
|
+
meta: {
|
|
7
|
+
block: /\/\/\s*==UserScript==([\s\S]*?)\/\/\s*==\/UserScript==/,
|
|
8
|
+
license: /^[ \t]*\/\/[ \t]*@license[ \t]+(\S.*)/m
|
|
9
|
+
},
|
|
10
|
+
newline: /\r?\n/,
|
|
11
|
+
specialChars: /[.*+?^${}()|[\]\\]/g,
|
|
12
|
+
url: /^https?:\/\//,
|
|
13
|
+
whitespace: { all: /\s+/, leading: /^\s*/ }
|
|
14
|
+
}
|
package/src/index.d.ts
CHANGED
package/src/rules/meta-order.js
CHANGED
|
@@ -12,7 +12,7 @@ function findViolation(entries, keys) {
|
|
|
12
12
|
const key = entry.key.split(':', 1)[0]
|
|
13
13
|
return { entry, key, rank: ranks.get(key) }
|
|
14
14
|
}).filter(item => item.rank !== undefined)
|
|
15
|
-
for (let i = 1 ; i < known.length ; i++) //
|
|
15
|
+
for (let i = 1 ; i < known.length ; i++) // oxlint-disable-next-line security/detect-object-injection
|
|
16
16
|
if (known[i -1].rank > known[i].rank) return [known[i -1], known[i]]
|
|
17
17
|
}
|
|
18
18
|
|
|
@@ -58,7 +58,7 @@ export default {
|
|
|
58
58
|
if (!violation) return
|
|
59
59
|
const [first, second] = violation
|
|
60
60
|
const sorted = sortEntries(sortEntries(entries, master), custom)
|
|
61
|
-
const replacements = //
|
|
61
|
+
const replacements = // oxlint-disable-next-line security/detect-object-injection
|
|
62
62
|
new Map(sorted.map((entry, idx) => [entries[idx], sourceCode.getText(entry.comment)]))
|
|
63
63
|
const start = entries[0].comment.range[0]
|
|
64
64
|
const end = entries.at(-1).comment.range[1]
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import metadata from '../utils/metadata.js'
|
|
2
|
-
|
|
3
|
-
const re = { leadingWhitespace: /^\s*/, whitespace: /\s+/ }
|
|
2
|
+
import regex from '../data/regex.js'
|
|
4
3
|
|
|
5
4
|
export default {
|
|
6
5
|
meta: {
|
|
@@ -35,24 +34,26 @@ export default {
|
|
|
35
34
|
let targetCol = metaEntries.reduce((max, entry) => Math.max(max, entry.key.length), 0) + minSpaces +4
|
|
36
35
|
for (const entry of metaEntries)
|
|
37
36
|
if (entry.key == 'resource' && entry.value.trim()) {
|
|
38
|
-
const name = entry.value.trim().split(
|
|
37
|
+
const name = entry.value.trim().split(regex.whitespace.all)[0],
|
|
39
38
|
requiredCol = 4 + entry.key.length +1 + name.length + minSpaces
|
|
40
39
|
if (requiredCol > targetCol) targetCol = requiredCol
|
|
41
40
|
}
|
|
42
41
|
for (const entry of metaEntries) {
|
|
43
42
|
if (!entry.value.trim()) continue
|
|
44
43
|
const commentText = sourceCode.getText(entry.comment),
|
|
45
|
-
|
|
46
|
-
|
|
44
|
+
trimmed = commentText.trimEnd(),
|
|
45
|
+
trailingWs = commentText.slice(trimmed.length),
|
|
46
|
+
keyIdx = trimmed.indexOf(entry.key) + entry.key.length,
|
|
47
|
+
parts = entry.value.trim().split(regex.whitespace.all),
|
|
47
48
|
named = entry.key == 'resource' && parts.length >= 2,
|
|
48
49
|
prefix = named ? ` ${parts[0]} ` : '',
|
|
49
50
|
suffix = named ? parts.slice(1).join(' ') : entry.value.trim(),
|
|
50
|
-
fixedText =
|
|
51
|
+
fixedText = trimmed.slice(0, keyIdx) + prefix
|
|
51
52
|
+ ' '.repeat(targetCol - keyIdx - prefix.length) + suffix
|
|
52
|
-
if (
|
|
53
|
+
if (trimmed !== fixedText)
|
|
53
54
|
context.report({
|
|
54
55
|
node: entry.comment, messageId: 'misaligned', data: { column: targetCol },
|
|
55
|
-
fix(fixer) { return fixer.replaceText(entry.comment, fixedText) }
|
|
56
|
+
fix(fixer) { return fixer.replaceText(entry.comment, fixedText + trailingWs) }
|
|
56
57
|
})
|
|
57
58
|
}
|
|
58
59
|
}
|
|
@@ -1,14 +1,30 @@
|
|
|
1
|
-
import
|
|
1
|
+
import headers from '../data/headers.js'
|
|
2
|
+
import regex from '../data/regex.js'
|
|
2
3
|
|
|
3
4
|
const re = {
|
|
4
5
|
insecureURL: /^http:\/\/(?!localhost\b|127\.0\.0\.1\b|\[::1\])/i,
|
|
5
6
|
meta: {
|
|
6
7
|
end: /^\/\/\s*==\/UserScript==\s*$/,
|
|
7
|
-
line: /^\/\/\s*@([\w-]+)(?::[\w-]+)?\s+(\S.*)$/, //
|
|
8
|
+
line: /^\/\/\s*@([\w-]+)(?::[\w-]+)?\s+(\S.*)$/, // oxlint-disable-line security/detect-unsafe-regex
|
|
8
9
|
start: /^\/\/\s*==UserScript==\s*$/
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function isInsecure(url) { return typeof url == 'string' && re.insecureURL.test(url) }
|
|
14
|
+
function urlFromLiteral(node) {
|
|
15
|
+
return (node?.type == 'Literal'
|
|
16
|
+
&& typeof node.value == 'string'
|
|
17
|
+
&& isInsecure(node.value)
|
|
18
|
+
) ? node.value : null
|
|
19
|
+
}
|
|
20
|
+
function urlFromOptions(node) {
|
|
21
|
+
if (node?.type != 'ObjectExpression') return null
|
|
22
|
+
for (const prop of node.properties)
|
|
23
|
+
if (prop.type == 'Property' && (prop.key?.name == 'url' || prop.key?.value == 'url')) {
|
|
24
|
+
const val = prop.value
|
|
25
|
+
if (val?.type == 'Literal' && isInsecure(val.value)) return val.value
|
|
26
|
+
}
|
|
27
|
+
return null
|
|
12
28
|
}
|
|
13
29
|
|
|
14
30
|
export default {
|
|
@@ -32,9 +48,8 @@ export default {
|
|
|
32
48
|
create(context) {
|
|
33
49
|
const xhrVars = new Set(),
|
|
34
50
|
sourceCode = context.sourceCode ?? context.getSourceCode()
|
|
35
|
-
function isInsecure(url) { return typeof url == 'string' && re.insecureURL.test(url) }
|
|
36
51
|
function reportMetadata() {
|
|
37
|
-
const lines = sourceCode.text.split(
|
|
52
|
+
const lines = sourceCode.text.split(regex.newline)
|
|
38
53
|
let inBlock = false
|
|
39
54
|
for (const [i, rawLine] of lines.entries()) {
|
|
40
55
|
const line = rawLine.trimEnd()
|
|
@@ -43,8 +58,8 @@ export default {
|
|
|
43
58
|
if (!inBlock) continue
|
|
44
59
|
const match = line.match(re.meta.line) ; if (!match) continue
|
|
45
60
|
const key = match[1]
|
|
46
|
-
const type =
|
|
47
|
-
const tokens = match[2].split(
|
|
61
|
+
const type = headers.url.get(key) ; if (!type) continue
|
|
62
|
+
const tokens = match[2].split(regex.whitespace.all),
|
|
48
63
|
url = type == 'named-url' ? tokens[1] : tokens[0]
|
|
49
64
|
if (!url || !isInsecure(url)) continue
|
|
50
65
|
const col = line.lastIndexOf(url)
|
|
@@ -62,22 +77,6 @@ export default {
|
|
|
62
77
|
: node.callee.name
|
|
63
78
|
context.report({ node: node.arguments[0], messageId: 'insecureGMXHR', data: { callee, url }})
|
|
64
79
|
}
|
|
65
|
-
function urlFromLiteral(node) {
|
|
66
|
-
return (node?.type == 'Literal'
|
|
67
|
-
&& typeof node.value == 'string'
|
|
68
|
-
&& isInsecure(node.value)
|
|
69
|
-
) ? node.value : null
|
|
70
|
-
}
|
|
71
|
-
function urlFromOptions(node) {
|
|
72
|
-
if (node?.type != 'ObjectExpression') return null
|
|
73
|
-
for (const prop of node.properties)
|
|
74
|
-
if (prop.type == 'Property' && (prop.key?.name == 'url' || prop.key?.value == 'url')) {
|
|
75
|
-
const val = prop.value
|
|
76
|
-
if (val?.type == 'Literal' && isInsecure(val.value)) return val.value
|
|
77
|
-
}
|
|
78
|
-
return null
|
|
79
|
-
}
|
|
80
|
-
|
|
81
80
|
return {
|
|
82
81
|
Program() { reportMetadata() },
|
|
83
82
|
VariableDeclarator(node) {
|
|
@@ -3,6 +3,8 @@ const re = {
|
|
|
3
3
|
postmsgValidation: /\b\w+\.(?:origin|source)\b/
|
|
4
4
|
}
|
|
5
5
|
|
|
6
|
+
function isMessageTarget(obj) { return obj?.type == 'Identifier' && re.messageTargets.has(obj.name) }
|
|
7
|
+
|
|
6
8
|
export default {
|
|
7
9
|
meta: {
|
|
8
10
|
type: 'problem',
|
|
@@ -41,9 +43,6 @@ export default {
|
|
|
41
43
|
if (re.postmsgValidation.test(body)) return
|
|
42
44
|
context.report({ node, messageId: 'unvalidatedPostMessage', data: { target }})
|
|
43
45
|
}
|
|
44
|
-
function isMessageTarget(obj) {
|
|
45
|
-
return obj?.type == 'Identifier' && re.messageTargets.has(obj.name)
|
|
46
|
-
}
|
|
47
46
|
return {
|
|
48
47
|
CallExpression(node) {
|
|
49
48
|
if (node.callee.type != 'MemberExpression') return
|
|
@@ -1,10 +1,11 @@
|
|
|
1
|
+
import api from '../utils/api.js'
|
|
1
2
|
import metadata from '../utils/metadata.js'
|
|
2
3
|
|
|
3
|
-
import
|
|
4
|
+
import apis from '../data/apis.js'
|
|
5
|
+
import headers from '../data/headers.js'
|
|
4
6
|
|
|
5
|
-
const
|
|
6
|
-
|
|
7
|
-
allGrants = new Set([...allAPIs, ...specialGrants]),
|
|
7
|
+
const specialGrants = new Set(['none', 'unsafeWindow', 'window.close', 'window.focus', 'window.onurlchange']),
|
|
8
|
+
allGrants = new Set([...apis.all, ...specialGrants]),
|
|
8
9
|
sortedGrants = new Map(Array.from(allGrants, grant => [grant.toLowerCase(), grant]))
|
|
9
10
|
|
|
10
11
|
export default {
|
|
@@ -20,8 +21,9 @@ export default {
|
|
|
20
21
|
properties: {
|
|
21
22
|
target: {
|
|
22
23
|
anyOf: [
|
|
23
|
-
{ type: 'string', enum: ['all', ...managers] },
|
|
24
|
-
{ type: 'array', items: { type: 'string', enum: managers },
|
|
24
|
+
{ type: 'string', enum: ['all', ...headers.managers] },
|
|
25
|
+
{ type: 'array', items: { type: 'string', enum: headers.managers },
|
|
26
|
+
minItems: 1, uniqueItems: true }
|
|
25
27
|
],
|
|
26
28
|
description: 'Manager(s) whose grants to validate against (default: all)'
|
|
27
29
|
},
|
|
@@ -67,9 +69,9 @@ export default {
|
|
|
67
69
|
continue
|
|
68
70
|
}
|
|
69
71
|
if (!targets.includes('all')
|
|
70
|
-
&& !targets.some(manager =>
|
|
72
|
+
&& !targets.some(manager => api.supports({ manager, api: grantName }))
|
|
71
73
|
) {
|
|
72
|
-
const supporting = managers.filter(manager =>
|
|
74
|
+
const supporting = headers.managers.filter(manager => api.supports({ manager, api: grantName }))
|
|
73
75
|
context.report({ node: grant.comment, messageId: 'unsupportedGrant',
|
|
74
76
|
data: { grant: grantName, target: targets.join(', '), supporting: supporting.join(', ') }})
|
|
75
77
|
}
|
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
import metadata from '../utils/metadata.js'
|
|
2
|
-
|
|
3
|
-
import { managerHeaders, managers, localizedHeaders, validHeaders } from '../data/headers.js'
|
|
2
|
+
import headers from '../data/headers.js'
|
|
4
3
|
|
|
5
4
|
function parseAllowedPattern(entry) {
|
|
6
5
|
if (typeof entry != 'string' || !entry.startsWith('/')) return null
|
|
7
6
|
const lastSlash = entry.lastIndexOf('/')
|
|
8
7
|
if (lastSlash == 0) return null
|
|
9
|
-
try { /*
|
|
8
|
+
try { /* oxlint-disable-next-line security/detect-non-literal-regexp */
|
|
10
9
|
return new RegExp(entry.slice(1, lastSlash), entry.slice(lastSlash +1))
|
|
11
10
|
} catch { return null }
|
|
12
11
|
}
|
|
@@ -24,8 +23,9 @@ export default {
|
|
|
24
23
|
properties: {
|
|
25
24
|
target: {
|
|
26
25
|
anyOf: [
|
|
27
|
-
{ type: 'string', enum: ['all', ...managers] },
|
|
28
|
-
{ type: 'array', items: {
|
|
26
|
+
{ type: 'string', enum: ['all', ...headers.managers] },
|
|
27
|
+
{ type: 'array', items: {
|
|
28
|
+
type: 'string', enum: headers.managers }, minItems: 1, uniqueItems: true }
|
|
29
29
|
],
|
|
30
30
|
description: 'Manager(s) whose headers to validate against (default: all)'
|
|
31
31
|
},
|
|
@@ -50,8 +50,8 @@ export default {
|
|
|
50
50
|
allowedList = [options.allowedHeaders ?? []].flat(),
|
|
51
51
|
allowedExact = new Set(),
|
|
52
52
|
allowedRegexes = [],
|
|
53
|
-
validSet = targets.includes('all') ?
|
|
54
|
-
: new Set(targets.flatMap(target => [...
|
|
53
|
+
validSet = targets.includes('all') ? headers.valid.get('all')
|
|
54
|
+
: new Set(targets.flatMap(target => [...headers.valid.get(target)]))
|
|
55
55
|
for (const entry of allowedList) {
|
|
56
56
|
if (entry instanceof RegExp) allowedRegexes.push(entry)
|
|
57
57
|
else if (typeof entry == 'string') {
|
|
@@ -66,9 +66,9 @@ export default {
|
|
|
66
66
|
for (const entry of metaEntries) {
|
|
67
67
|
if (allowedExact.has(entry.key)) continue
|
|
68
68
|
if (allowedRegexes.some(re => re.test(entry.key))) continue
|
|
69
|
-
if (
|
|
69
|
+
if (headers.localized.includes(entry.key.split(':')[0])) continue
|
|
70
70
|
if (validSet.has(entry.key)) continue
|
|
71
|
-
const supporting =
|
|
71
|
+
const supporting = headers.manager.get(entry.key)
|
|
72
72
|
if (supporting)
|
|
73
73
|
context.report({ node: entry.comment, messageId: 'unsupportedHeader',
|
|
74
74
|
data: { header: entry.key, target: targets.join(', '), supporting: supporting.join(', ') }})
|
|
@@ -1,9 +1,7 @@
|
|
|
1
1
|
import metadata from '../utils/metadata.js'
|
|
2
|
+
import apis from '../data/apis.js'
|
|
2
3
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
const apis = new Set([...gmLegacyAPIs, ...gmModernAPIs, ...scAPIs]),
|
|
6
|
-
grantlessAPIs = new Set(['GM_info', 'GM.info']) // always available, no grant required
|
|
4
|
+
apis.grantless = new Set(['GM_info', 'GM.info'])
|
|
7
5
|
|
|
8
6
|
export default {
|
|
9
7
|
meta: {
|
|
@@ -19,7 +17,7 @@ export default {
|
|
|
19
17
|
|
|
20
18
|
create(context) {
|
|
21
19
|
const usedAPIs = new Map()
|
|
22
|
-
function record(name, node) { if (apis.has(name) && !usedAPIs.has(name)) usedAPIs.set(name, node) }
|
|
20
|
+
function record(name, node) { if (apis.all.has(name) && !usedAPIs.has(name)) usedAPIs.set(name, node) }
|
|
23
21
|
return {
|
|
24
22
|
Identifier(node) { record(node.name, node) },
|
|
25
23
|
MemberExpression(node) {
|
|
@@ -42,7 +40,7 @@ export default {
|
|
|
42
40
|
.map(entry => entry.value.trim())
|
|
43
41
|
)
|
|
44
42
|
for (const [api, node] of usedAPIs) {
|
|
45
|
-
if (
|
|
43
|
+
if (apis.grantless.has(api) || declared.has(api)) continue
|
|
46
44
|
context.report({ node, messageId: 'missingGrant', data: { api }})
|
|
47
45
|
}
|
|
48
46
|
}
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import metadata from '../utils/metadata.js'
|
|
2
2
|
import template from '../utils/template.js'
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
|
|
4
|
+
import regex from '../data/regex.js'
|
|
5
|
+
|
|
6
|
+
const resourceGetterNames = new Set(['GM_getResourceText', 'GM_getResourceURL'])
|
|
6
7
|
|
|
7
8
|
export default {
|
|
8
9
|
meta: {
|
|
@@ -29,8 +30,8 @@ export default {
|
|
|
29
30
|
if (resolved) usedRefs.push({ names: resolved, pattern: null, node })
|
|
30
31
|
else usedRefs.push({
|
|
31
32
|
names: null,
|
|
32
|
-
pattern: new RegExp( //
|
|
33
|
-
`^${arg.quasis.map(q => q.value.raw.replace(
|
|
33
|
+
pattern: new RegExp( // oxlint-disable-line security/detect-non-literal-regexp
|
|
34
|
+
`^${arg.quasis.map(q => q.value.raw.replace(regex.specialChars, '\\$&')).join('.*')}$`),
|
|
34
35
|
node
|
|
35
36
|
})
|
|
36
37
|
}
|
|
@@ -45,7 +46,7 @@ export default {
|
|
|
45
46
|
const declared = new Set(
|
|
46
47
|
metadata.extract(context.sourceCode)
|
|
47
48
|
.filter(entry => entry.key == 'resource')
|
|
48
|
-
.map(entry => entry.value.trim().split(
|
|
49
|
+
.map(entry => entry.value.trim().split(regex.whitespace.all)[0])
|
|
49
50
|
)
|
|
50
51
|
for (const ref of usedRefs) {
|
|
51
52
|
if (ref.names) {
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import apis from '../data/apis.js'
|
|
2
|
+
import headers from '../data/headers.js'
|
|
3
|
+
import regex from '../data/regex.js'
|
|
4
|
+
|
|
5
|
+
const re = { connect: /^\s*\/\/\s*@connect\s+(\S+)/, grant: /^\s*\/\/\s*@grant\s+(\S+)/ }
|
|
6
|
+
|
|
7
|
+
export default {
|
|
8
|
+
meta: {
|
|
9
|
+
type: 'problem',
|
|
10
|
+
docs: {
|
|
11
|
+
description: 'Flags security-sensitive `@grant` values and overbroad `@connect` patterns',
|
|
12
|
+
category: 'Possible Errors',
|
|
13
|
+
recommended: false,
|
|
14
|
+
security: true
|
|
15
|
+
},
|
|
16
|
+
defaultOptions: [{ target: 'all', allowedGrants: [], allowBroadConnect: false }],
|
|
17
|
+
schema: [{
|
|
18
|
+
type: 'object',
|
|
19
|
+
properties: {
|
|
20
|
+
target: {
|
|
21
|
+
oneOf: [
|
|
22
|
+
{ type: 'string', enum: ['all', ...headers.managers], description: 'Single manager (or "all")' },
|
|
23
|
+
{ type: 'array', items: { type: 'string', enum: headers.managers },
|
|
24
|
+
description: 'Multiple managers' }
|
|
25
|
+
],
|
|
26
|
+
description: 'Manager(s) whose sensitive-grant set to check against'
|
|
27
|
+
},
|
|
28
|
+
allowedGrants: {
|
|
29
|
+
oneOf: [
|
|
30
|
+
{ type: 'string', description: 'Single sensitive grant to allow' },
|
|
31
|
+
{ type: 'array', items: { type: 'string' }, description: 'Multiple sensitive grants to allow' }
|
|
32
|
+
],
|
|
33
|
+
description: 'Sensitive grants to explicitly allow'
|
|
34
|
+
},
|
|
35
|
+
allowBroadConnect: { type: 'boolean', description: 'Permit @connect * and wildcard-domain patterns' }
|
|
36
|
+
},
|
|
37
|
+
additionalProperties: false
|
|
38
|
+
}],
|
|
39
|
+
messages: {
|
|
40
|
+
sensitiveGrant: '`@grant {{grant}}` is security-sensitive. Review whether it\'s needed'
|
|
41
|
+
+ ' or add it to `allowedGrants`.',
|
|
42
|
+
broadConnect: '`@connect {{pattern}}` allows `GM_xmlhttpRequest` to reach arbitrary hosts.'
|
|
43
|
+
+ ' Scope it to specific hosts or enable `allowBroadConnect`.'
|
|
44
|
+
}
|
|
45
|
+
},
|
|
46
|
+
|
|
47
|
+
create(context) {
|
|
48
|
+
const opts = context.options[0] ?? {},
|
|
49
|
+
target = opts.target ?? 'all',
|
|
50
|
+
allowBroadConnect = opts.allowBroadConnect === true,
|
|
51
|
+
allowed = new Set(typeof opts.allowedGrants == 'string' ? [opts.allowedGrants]
|
|
52
|
+
: opts.allowedGrants ?? []),
|
|
53
|
+
sourceCode = context.sourceCode ?? context.getSourceCode(),
|
|
54
|
+
managers = target == 'all' ? [...apis.managerSensitive.keys()]
|
|
55
|
+
: Array.isArray(target) ? target : [target]
|
|
56
|
+
const isSensitive = grant => apis.sensitive.has(grant)
|
|
57
|
+
|| managers.some(mgr => apis.managerSensitive.get(mgr)?.has(grant))
|
|
58
|
+
const report = (line, messageId, data, col, len) => context.report({
|
|
59
|
+
loc: { start: { line, column: col }, end: { line, column: col + len }}, messageId, data })
|
|
60
|
+
return {
|
|
61
|
+
Program() {
|
|
62
|
+
const match = sourceCode.text.match(regex.meta.block) ; if (!match) return
|
|
63
|
+
const lines = match[1].split(regex.newline),
|
|
64
|
+
baseLine = sourceCode.text.slice(0, match.index).split(regex.newline).length,
|
|
65
|
+
lineData = lines.map(line => ({
|
|
66
|
+
grant: line.match(re.grant)?.[1],
|
|
67
|
+
connect: line.match(re.connect)?.[1]
|
|
68
|
+
})),
|
|
69
|
+
hasXHR = lineData.some(d => d.grant == 'GM_xmlhttpRequest' || d.grant == 'GM.xmlHttpRequest')
|
|
70
|
+
for (const [i, { grant, connect }] of lineData.entries()) {
|
|
71
|
+
const lineNo = baseLine + i +1,
|
|
72
|
+
raw = lines[i] // oxlint-disable-line security/detect-object-injection
|
|
73
|
+
if (grant && !allowed.has(grant) && isSensitive(grant))
|
|
74
|
+
report(lineNo, 'sensitiveGrant', { grant }, raw.indexOf(grant), grant.length)
|
|
75
|
+
if (connect && hasXHR && !allowBroadConnect && headers.broadConnect.has(connect))
|
|
76
|
+
report(lineNo, 'broadConnect', { pattern: connect }, raw.indexOf(connect), connect.length)
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|