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/src/data/apis.js CHANGED
@@ -1,54 +1,57 @@
1
- export const gmLegacyAPIs = new Set([ /* eslint-disable no-secrets/no-secrets */
2
- 'GM_addElement', 'GM_addStyle', 'GM_addValueChangeListener', 'GM_deleteValue', 'GM_download',
3
- 'GM_getResourceText', 'GM_getResourceURL', 'GM_getTab', 'GM_getTabs', 'GM_getValue', 'GM_getValues',
4
- 'GM_info', 'GM_listValues', 'GM_log', 'GM_notification', 'GM_openInTab', 'GM_registerMenuCommand',
5
- 'GM_removeValueChangeListener', 'GM_saveTab', 'GM_setClipboard', 'GM_setValue', 'GM_setValues',
6
- 'GM_unregisterMenuCommand', 'GM_webRequest', 'GM_xmlhttpRequest'
7
- ]) /* eslint-enable no-secrets/no-secrets */
8
-
9
- export const gmModernAPIs = new Set([
10
- 'GM.addElement', 'GM.addStyle', 'GM.addValueChangeListener', 'GM.deleteValue', 'GM.download',
11
- 'GM.getResourceText', 'GM.getResourceUrl', 'GM.getTab', 'GM.getTabs', 'GM.getValue', 'GM.getValues',
12
- 'GM.info', 'GM.listValues', 'GM.log', 'GM.notification', 'GM.openInTab', 'GM.registerMenuCommand',
13
- 'GM.removeValueChangeListener', 'GM.saveTab', 'GM.setClipboard', 'GM.setValue', 'GM.setValues',
14
- 'GM.unregisterMenuCommand', 'GM.webRequest', 'GM.xmlHttpRequest'
15
- ])
16
-
17
- export const scAPIs = new Set([
18
- 'CAT.agent.conversation', 'CAT.agent.dom', 'CAT.agent.mcp', 'CAT.agent.model',
19
- 'CAT.agent.opfs', 'CAT.agent.skills', 'CAT.agent.task',
20
- 'CAT_fileStorage', 'CAT_scriptLoaded', 'CAT_userConfig'
21
- ])
22
-
23
- const tmAPIs = new Set([
24
- 'GM_getTab', 'GM_getTabs', 'GM_saveTab', 'GM_webRequest',
25
- 'GM.getTab', 'GM.getTabs', 'GM.saveTab', 'GM.webRequest'
26
- ])
27
-
28
- const agAPIs = new Set([
29
- 'GM_info', 'GM.info',
30
- 'GM_setValue', 'GM.setValue',
31
- 'GM_getValue', 'GM.getValue',
32
- 'GM_listValues', 'GM.listValues',
33
- 'GM_deleteValue', 'GM.deleteValue',
34
- 'GM_getResourceURL', 'GM.getResourceUrl',
35
- 'GM_setClipboard', 'GM.setClipboard',
36
- 'GM_xmlhttpRequest', 'GM.xmlHttpRequest',
37
- 'GM_openInTab', 'GM.openInTab',
38
- 'GM_notification',
39
- 'unsafeWindow',
40
- 'GM_getResourceText',
41
- 'GM_addStyle',
42
- 'GM_log',
43
- 'GM_addElement', 'GM.addElement'
44
- ])
45
-
46
- export const allAPIs = new Set([...gmLegacyAPIs, ...gmModernAPIs, ...scAPIs])
47
-
48
- export function supportsAPI({ manager, api }) {
49
- return !allAPIs.has(api) ? false
50
- : api.startsWith('CAT') ? manager == 'scriptcat'
51
- : tmAPIs.has(api) ? manager == 'tampermonkey'
52
- : manager == 'adguard' ? agAPIs.has(api)
53
- : true
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
@@ -1,41 +1,51 @@
1
- export const universalHeaders = new Set([
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
- export const managerHeaders = new Map([
9
- ['unwrap', ['tampermonkey', 'violentmonkey', 'greasemonkey', 'scriptcat']],
10
- ['run-in', ['tampermonkey', 'scriptcat']],
11
- ['nocompat', ['tampermonkey']],
12
- ['sandbox', ['tampermonkey']],
13
- ['user-agent', ['tampermonkey']],
14
- ['webRequest', ['tampermonkey']],
15
- ['inject-into', ['violentmonkey', 'scriptcat']],
16
- ['top-level-await', ['violentmonkey']],
17
- ['background', ['scriptcat']],
18
- ['crontab', ['scriptcat']],
19
- ['definition', ['scriptcat']],
20
- ['early-start', ['scriptcat']],
21
- ['require-css', ['scriptcat']],
22
- ['storageName', ['scriptcat']]
23
- ])
24
-
25
- export const localizedHeaders = ['name', 'description', 'antifeature']
26
- export const managers = ['tampermonkey', 'violentmonkey', 'greasemonkey', 'scriptcat', 'adguard']
27
- export const validHeaders = new Map()
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
- validHeaders.set('all', new Set([...universalHeaders, ...managerHeaders.keys()]))
38
+ const knownHeaders = new Set([...headers.universal, ...headers.manager.keys()])
30
39
 
31
- for (const manager of managers) {
32
- const specific = [...managerHeaders]
33
- .filter(([, supportedBy]) => supportedBy.includes(manager))
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
- validHeaders.set(manager, new Set([...universalHeaders, ...specific]))
45
+ headers.valid.set(mgr, new Set([...headers.universal, ...specific]))
36
46
  }
37
47
 
38
- const urlHeaderTypes = {
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
- const knownHeaders = new Set([...universalHeaders, ...managerHeaders.keys()])
60
- export const urlHeaders = new Map(Object.entries(urlHeaderTypes).filter(([name]) => knownHeaders.has(name)))
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
@@ -29,6 +29,7 @@ interface PluginRule {
29
29
  }
30
30
 
31
31
  declare const plugin: {
32
+ meta: { name: string }
32
33
  rules: Record<string, PluginRule>
33
34
  configs: Record<ConfigName, PluginConfig>
34
35
  }
@@ -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++) // eslint-disable-next-line security/detect-object-injection
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 = // eslint-disable-next-line security/detect-object-injection
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(re.whitespace)[0],
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
- keyIdx = commentText.indexOf(entry.key) + entry.key.length,
46
- parts = entry.value.trim().split(re.whitespace),
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 = commentText.slice(0, keyIdx) + prefix
51
+ fixedText = trimmed.slice(0, keyIdx) + prefix
51
52
  + ' '.repeat(targetCol - keyIdx - prefix.length) + suffix
52
- if (commentText !== fixedText)
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 { urlHeaders } from '../data/headers.js'
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.*)$/, // eslint-disable-line security/detect-unsafe-regex
8
+ line: /^\/\/\s*@([\w-]+)(?::[\w-]+)?\s+(\S.*)$/, // oxlint-disable-line security/detect-unsafe-regex
8
9
  start: /^\/\/\s*==UserScript==\s*$/
9
- },
10
- newline: /\r?\n/,
11
- whitespace: /\s+/
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(re.newline)
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 = urlHeaders.get(key) ; if (!type) continue
47
- const tokens = match[2].split(re.whitespace),
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 { allAPIs, supportsAPI } from '../data/apis.js'
4
+ import apis from '../data/apis.js'
5
+ import headers from '../data/headers.js'
4
6
 
5
- const managers = ['tampermonkey', 'violentmonkey', 'greasemonkey', 'scriptcat', 'adguard'],
6
- specialGrants = new Set(['none', 'unsafeWindow', 'window.close', 'window.focus', 'window.onurlchange']),
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 }, minItems: 1, uniqueItems: true }
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 => supportsAPI({ manager, api: grantName }))
72
+ && !targets.some(manager => api.supports({ manager, api: grantName }))
71
73
  ) {
72
- const supporting = managers.filter(manager => supportsAPI({ manager, api: grantName }))
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 { /* eslint-disable-next-line security/detect-non-literal-regexp */
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: { type: 'string', enum: managers }, minItems: 1, uniqueItems: true }
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') ? validHeaders.get('all')
54
- : new Set(targets.flatMap(target => [...validHeaders.get(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 (localizedHeaders.includes(entry.key.split(':')[0])) continue
69
+ if (headers.localized.includes(entry.key.split(':')[0])) continue
70
70
  if (validSet.has(entry.key)) continue
71
- const supporting = managerHeaders.get(entry.key)
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
- import { gmLegacyAPIs, gmModernAPIs, scAPIs } from '../data/apis.js'
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 (grantlessAPIs.has(api) || declared.has(api)) continue
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
- const resourceGetterNames = new Set(['GM_getResourceText', 'GM_getResourceURL']),
5
- re = { specialChars: /[.*+?^${}()|[\]\\]/g, whitespace: /\s+/ }
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( // eslint-disable-line security/detect-non-literal-regexp
33
- `^${arg.quasis.map(q => q.value.raw.replace(re.specialChars, '\\$&')).join('.*')}$`),
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(re.whitespace)[0])
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
+ }