biome-plugin-suitescript 0.1.1 → 0.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/README.md +157 -27
- package/biome-manifest.json +261 -7
- package/configs/all.json +29 -0
- package/configs/minimal.json +14 -0
- package/configs/recommended.json +20 -0
- package/dist/cli/analyze.js +33 -3
- package/dist/lib/define.js +35 -5
- package/dist/lib/headers.js +12 -0
- package/dist/lib/module-calls.js +65 -0
- package/dist/lib/module-script-types.js +76 -0
- package/dist/lib/options.js +35 -0
- package/dist/lib/required-options.js +14 -0
- package/dist/lib/script-side.js +83 -0
- package/dist/lib/script-types.js +37 -14
- package/dist/lib/suiteql.js +16 -0
- package/dist/lib/suitescript.js +23 -0
- package/dist/lib/typings-data.js +4327 -0
- package/dist/lib/typings.js +2 -0
- package/dist/rules/logRequireDetails.js +4 -0
- package/dist/rules/logRequireTitle.js +1 -0
- package/dist/rules/noAuthorizationHeaderInInternalRequest.js +24 -0
- package/dist/rules/noBeforeSubmitRecordReload.js +19 -0
- package/dist/rules/noBlockedHttpsHeader.js +32 -0
- package/dist/rules/noBlockedResponseHeader.js +35 -0
- package/dist/rules/noBrowserGlobals.js +25 -0
- package/dist/rules/noClientLog.js +14 -0
- package/dist/rules/noConflictingClientScriptProps.js +36 -0
- package/dist/rules/noDynamicRequire.js +23 -0
- package/dist/rules/noDynamicSuiteQL.js +29 -0
- package/dist/rules/noEmptyEntryPoints.js +19 -0
- package/dist/rules/noExtraDeps.js +16 -0
- package/dist/rules/noExtraParams.js +17 -0
- package/dist/rules/noGlobalShadowing.js +38 -0
- package/dist/rules/noHardcodedAuthPasswords.js +27 -0
- package/dist/rules/noHardcodedSecrets.js +43 -0
- package/dist/rules/noHeaderUnderscores.js +23 -0
- package/dist/rules/noInnerHtml.js +41 -0
- package/dist/rules/noInsecureCookie.js +27 -0
- package/dist/rules/noInsecureHttpsUrl.js +20 -0
- package/dist/rules/noInvalidDefine.js +47 -0
- package/dist/rules/noInvalidEnumMember.js +24 -0
- package/dist/rules/noLocationHeader.js +20 -0
- package/dist/rules/noLogModule.js +8 -2
- package/dist/rules/noMapReduceSearchResults.js +33 -0
- package/dist/rules/noMissingModules.js +29 -0
- package/dist/rules/noModuleScopeApiCalls.js +49 -0
- package/dist/rules/noMultipleDefine.js +14 -0
- package/dist/rules/noNativeIterators.js +19 -0
- package/dist/rules/noPositionalOptions.js +26 -0
- package/dist/rules/noPostMessageWildcard.js +15 -0
- package/dist/rules/noPublicFileUpload.js +20 -0
- package/dist/rules/noQueryColumnFieldAndFormula.js +19 -0
- package/dist/rules/noRecordInLoop.js +34 -0
- package/dist/rules/noRequireInsteadOfDefine.js +25 -0
- package/dist/rules/noSecretKeyGuidAndSecret.js +16 -0
- package/dist/rules/noSensitiveBrowserStorage.js +24 -0
- package/dist/rules/noStackInResponse.js +21 -0
- package/dist/rules/noSuiteQLSelectStar.js +18 -0
- package/dist/rules/noSuiteQLSystemNoteJoin.js +19 -0
- package/dist/rules/noSuiteQLTypeInSearch.js +41 -0
- package/dist/rules/noSuiteScript1Api.js +84 -0
- package/dist/rules/noUnknownModuleMember.js +23 -0
- package/dist/rules/noUnknownOptions.js +31 -0
- package/dist/rules/noUnsupportedServerWidgetFieldType.js +40 -0
- package/dist/rules/noWeakHash.js +17 -0
- package/dist/rules/noWildcardCors.js +16 -0
- package/dist/rules/noWrongScriptTypeModules.js +30 -0
- package/dist/rules/requireClientValidateReturn.js +27 -0
- package/dist/rules/requireCustpagePrefix.js +23 -0
- package/dist/rules/requireEachReturn.js +38 -0
- package/dist/rules/requireLogModule.js +25 -0
- package/dist/rules/requireOptions.js +26 -0
- package/dist/rules/requireOrderByForPagedSuiteQL.js +17 -0
- package/dist/rules/requireQueryOptions.js +53 -0
- package/dist/rules/requireServerResponseOptions.js +33 -0
- package/dist/rules/requireServerWidgetOptions.js +37 -0
- package/package.json +3 -1
- package/dist/rules/noExtraModules.js +0 -23
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import { ast, defineRule, registerDiagnostic } from "@biomejs/runtime/plugin";
|
|
2
2
|
import { hasProperty, logCall } from "../lib/log.js";
|
|
3
|
+
// Tier 0: docs/adr/0002-options-are-separate-rules.md.
|
|
4
|
+
// Oracle's help center marks log `details` optional on all four log method pages;
|
|
5
|
+
// requiring it does not contradict that, it is a house practice from the ESLint
|
|
6
|
+
// log-args rules. As the lowest-precedence kind of source it is in `all` only.
|
|
3
7
|
export const logRequireDetails = defineRule({
|
|
4
8
|
query: ast("JS_CALL_EXPRESSION"),
|
|
5
9
|
run(node) {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { ast, defineRule, registerDiagnostic } from "@biomejs/runtime/plugin";
|
|
2
2
|
import { hasProperty, logCall } from "../lib/log.js";
|
|
3
|
+
// Tier 0: docs/adr/0002-options-are-separate-rules.md.
|
|
3
4
|
export const logRequireTitle = defineRule({
|
|
4
5
|
query: ast("JS_CALL_EXPRESSION"),
|
|
5
6
|
run(node) {
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { ast, defineRule, registerDiagnostic } from "@biomejs/runtime/plugin";
|
|
2
|
+
import { objectMembers, optionsObject, propertyValue } from "../lib/options.js";
|
|
3
|
+
const INTERNAL = /^https\.(requestRestlet|requestSuitelet|requestSuiteTalkRest)(\.promise)?$/;
|
|
4
|
+
// Oracle (https.requestRestlet, requestSuitelet, requestSuiteTalkRest): "Sends an
|
|
5
|
+
// HTTPS request to a RESTlet and returns the response. Authentication headers are
|
|
6
|
+
// automatically added." (the type definitions in N/https.d.ts say the same) and the error SSS_AUTHORIZATION_HEADER_NOT_ALLOWED: "The
|
|
7
|
+
// authorization header is set."
|
|
8
|
+
export const noAuthorizationHeaderInInternalRequest = defineRule({
|
|
9
|
+
query: ast("JS_CALL_EXPRESSION"),
|
|
10
|
+
run(node) {
|
|
11
|
+
if (!INTERNAL.test(node.callee?.text ?? ""))
|
|
12
|
+
return;
|
|
13
|
+
const options = optionsObject(node);
|
|
14
|
+
const headers = options && propertyValue(options, "headers");
|
|
15
|
+
if (headers?.kind !== "JS_OBJECT_EXPRESSION")
|
|
16
|
+
return;
|
|
17
|
+
for (const member of objectMembers(headers)) {
|
|
18
|
+
const name = member.children()[0]?.text.replace(/^['"]|['"]$/g, "");
|
|
19
|
+
if (name?.toLowerCase() === "authorization") {
|
|
20
|
+
registerDiagnostic(member, "error", "Do not set an Authorization header; NetSuite adds authentication to internal requests.");
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
});
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { ast, defineRule, registerDiagnostic } from "@biomejs/runtime/plugin";
|
|
2
|
+
import { enclosingFunctionNames } from "../lib/script-side.js";
|
|
3
|
+
const RECORD_CALLS = ["record.load", "record.submitFields", "record.delete"];
|
|
4
|
+
// Reloading or editing the record being saved from its own beforeSubmit fails
|
|
5
|
+
// with "Record has been changed". Only calls whose id is the context's
|
|
6
|
+
// newRecord.id are reported; other records are fine.
|
|
7
|
+
export const noBeforeSubmitRecordReload = defineRule({
|
|
8
|
+
query: ast("JS_CALL_EXPRESSION"),
|
|
9
|
+
run(node) {
|
|
10
|
+
if (!RECORD_CALLS.includes(node.callee?.text ?? ""))
|
|
11
|
+
return;
|
|
12
|
+
const options = node.arguments?.children()[0]?.children()[0];
|
|
13
|
+
if (!options || !/\bid\s*:\s*[\w.]*newRecord\.id\b/.test(options.text))
|
|
14
|
+
return;
|
|
15
|
+
if (!enclosingFunctionNames(node).includes("beforeSubmit"))
|
|
16
|
+
return;
|
|
17
|
+
registerDiagnostic(node, "error", `${node.callee?.text}() on the record being saved is not allowed in beforeSubmit.`);
|
|
18
|
+
},
|
|
19
|
+
});
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { ast, defineRule, registerDiagnostic } from "@biomejs/runtime/plugin";
|
|
2
|
+
import { objectMembers, optionsObject, propertyValue } from "../lib/options.js";
|
|
3
|
+
const BLOCKED = new Set([
|
|
4
|
+
"connection",
|
|
5
|
+
"content-length",
|
|
6
|
+
"host",
|
|
7
|
+
"jsessionid",
|
|
8
|
+
"trailer",
|
|
9
|
+
"transfer-encoding",
|
|
10
|
+
"upgrade",
|
|
11
|
+
"via",
|
|
12
|
+
]);
|
|
13
|
+
// Oracle (N/https, General Blocked HTTPS Headers): "certain headers cannot be set
|
|
14
|
+
// manually when using N/https module methods. If a script attempts to set values
|
|
15
|
+
// for any of the following headers, the values are discarded."
|
|
16
|
+
export const noBlockedHttpsHeader = defineRule({
|
|
17
|
+
query: ast("JS_CALL_EXPRESSION"),
|
|
18
|
+
run(node) {
|
|
19
|
+
if (!/^https\.\w+(\.promise)?$/.test(node.callee?.text ?? ""))
|
|
20
|
+
return;
|
|
21
|
+
const options = optionsObject(node);
|
|
22
|
+
const headers = options && propertyValue(options, "headers");
|
|
23
|
+
if (headers?.kind !== "JS_OBJECT_EXPRESSION")
|
|
24
|
+
return;
|
|
25
|
+
for (const member of objectMembers(headers)) {
|
|
26
|
+
const name = member.children()[0]?.text.replace(/^['"]|['"]$/g, "");
|
|
27
|
+
if (name && BLOCKED.has(name.toLowerCase())) {
|
|
28
|
+
registerDiagnostic(member, "warning", `NetSuite discards the ${name} header set in an N/https request.`);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
});
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { ast, defineRule, registerDiagnostic } from "@biomejs/runtime/plugin";
|
|
2
|
+
import { headerCall } from "../lib/headers.js";
|
|
3
|
+
const BLOCKED = new Set([
|
|
4
|
+
"allow",
|
|
5
|
+
"content-location",
|
|
6
|
+
"content-md5",
|
|
7
|
+
"content-range",
|
|
8
|
+
"date",
|
|
9
|
+
"location",
|
|
10
|
+
"proxy-authenticate",
|
|
11
|
+
"public-key-pins",
|
|
12
|
+
"public-key-pins-report-only",
|
|
13
|
+
"retry-after",
|
|
14
|
+
"server",
|
|
15
|
+
"strict-transport-security",
|
|
16
|
+
"upgrade-insecure-requests",
|
|
17
|
+
"warning",
|
|
18
|
+
"www-authenticate",
|
|
19
|
+
]);
|
|
20
|
+
// Oracle (N/https, HTTPS Header Information): "certain headers cannot be set
|
|
21
|
+
// manually when interacting with the https.ServerResponse Objects sent by
|
|
22
|
+
// Suitelets. If a script attempts to set values for any of these headers, the
|
|
23
|
+
// system throws an SSS_INVALID_HEADER error." Only calls on a receiver named like
|
|
24
|
+
// a response are checked.
|
|
25
|
+
export const noBlockedResponseHeader = defineRule({
|
|
26
|
+
query: ast("JS_CALL_EXPRESSION"),
|
|
27
|
+
run(node) {
|
|
28
|
+
if (!/response\.(setHeader|addHeader)$/i.test(node.callee?.text ?? ""))
|
|
29
|
+
return;
|
|
30
|
+
const header = headerCall(node);
|
|
31
|
+
if (header && BLOCKED.has(header.name)) {
|
|
32
|
+
registerDiagnostic(header.nameNode, "error", `A Suitelet cannot set the ${header.name} header; NetSuite throws SSS_INVALID_HEADER.`);
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
});
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { defineRule, registerDiagnostic, semantic, } from "@biomejs/runtime/plugin";
|
|
2
|
+
import { scriptSide } from "../lib/script-side.js";
|
|
3
|
+
const BROWSER_GLOBALS = new Set([
|
|
4
|
+
"document",
|
|
5
|
+
"window",
|
|
6
|
+
"localStorage",
|
|
7
|
+
"sessionStorage",
|
|
8
|
+
"XMLHttpRequest",
|
|
9
|
+
"alert",
|
|
10
|
+
"navigator",
|
|
11
|
+
"URLSearchParams",
|
|
12
|
+
"fetch",
|
|
13
|
+
]);
|
|
14
|
+
// Server scripts run outside a browser, but TypeScript's DOM library lets
|
|
15
|
+
// these names compile. Locals that reuse a name resolve to a binding and are left alone.
|
|
16
|
+
export const noBrowserGlobals = defineRule({
|
|
17
|
+
query: semantic("JS_REFERENCE_IDENTIFIER"),
|
|
18
|
+
run(node, { model }) {
|
|
19
|
+
if (!BROWSER_GLOBALS.has(node.text) || model.binding(node))
|
|
20
|
+
return;
|
|
21
|
+
if (scriptSide(node) !== "server")
|
|
22
|
+
return;
|
|
23
|
+
registerDiagnostic(node, "error", `'${node.text}' does not exist in server scripts.`);
|
|
24
|
+
},
|
|
25
|
+
});
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { ast, defineRule, registerDiagnostic } from "@biomejs/runtime/plugin";
|
|
2
|
+
import { logCall } from "../lib/log.js";
|
|
3
|
+
import { scriptSide } from "../lib/script-side.js";
|
|
4
|
+
// Oracle (N/log): in a client script attached to a form, "calls to any log
|
|
5
|
+
// function ... are ignored". It cannot tell whether a form is attached.
|
|
6
|
+
export const noClientLog = defineRule({
|
|
7
|
+
query: ast("JS_CALL_EXPRESSION"),
|
|
8
|
+
run(node) {
|
|
9
|
+
const info = logCall(node);
|
|
10
|
+
if (!info || scriptSide(node) !== "client")
|
|
11
|
+
return;
|
|
12
|
+
registerDiagnostic(node, "warning", `log.${info.prop}() is ignored in client scripts attached to a form; use console.`);
|
|
13
|
+
},
|
|
14
|
+
});
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { ast, defineRule, registerDiagnostic } from "@biomejs/runtime/plugin";
|
|
2
|
+
import { isFunction } from "../lib/script-side.js";
|
|
3
|
+
// Oracle (Form.clientScriptModulePath and clientScriptFileId): PROPERTY_VALUE_CONFLICT,
|
|
4
|
+
// "You attempted to set this value when the Form.clientScriptFileId property value
|
|
5
|
+
// has already been specified." Setting both on the same form fails.
|
|
6
|
+
export const noConflictingClientScriptProps = defineRule({
|
|
7
|
+
query: ast("JS_ASSIGNMENT_EXPRESSION"),
|
|
8
|
+
run(node) {
|
|
9
|
+
const target = node.children()[0];
|
|
10
|
+
if (target?.kind !== "JS_STATIC_MEMBER_ASSIGNMENT")
|
|
11
|
+
return;
|
|
12
|
+
const [object, member] = target.children();
|
|
13
|
+
if (member?.text !== "clientScriptModulePath" || !object)
|
|
14
|
+
return;
|
|
15
|
+
const scope = node.ancestors().find(isFunction);
|
|
16
|
+
if (!scope)
|
|
17
|
+
return;
|
|
18
|
+
const conflict = (function find(n) {
|
|
19
|
+
for (const child of n.children()) {
|
|
20
|
+
if (child.kind === "JS_ASSIGNMENT_EXPRESSION") {
|
|
21
|
+
const t = child.children()[0];
|
|
22
|
+
if (t?.kind === "JS_STATIC_MEMBER_ASSIGNMENT" &&
|
|
23
|
+
t.children()[1]?.text === "clientScriptFileId" &&
|
|
24
|
+
t.children()[0]?.text === object.text)
|
|
25
|
+
return true;
|
|
26
|
+
}
|
|
27
|
+
if (find(child))
|
|
28
|
+
return true;
|
|
29
|
+
}
|
|
30
|
+
return false;
|
|
31
|
+
})(scope);
|
|
32
|
+
if (conflict) {
|
|
33
|
+
registerDiagnostic(node, "error", `Set either clientScriptFileId or clientScriptModulePath on ${object.text}, not both.`);
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
});
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { ast, defineRule, registerDiagnostic } from "@biomejs/runtime/plugin";
|
|
2
|
+
import { callArguments } from "../lib/options.js";
|
|
3
|
+
// Oracle's OWASP skill: "Hardcode module paths; never build from input."
|
|
4
|
+
// A module name passed to require() must be a string literal, alone or in an
|
|
5
|
+
// array of them.
|
|
6
|
+
export const noDynamicRequire = defineRule({
|
|
7
|
+
query: ast("JS_CALL_EXPRESSION"),
|
|
8
|
+
run(node) {
|
|
9
|
+
if (node.callee?.text !== "require")
|
|
10
|
+
return;
|
|
11
|
+
const first = callArguments(node)[0];
|
|
12
|
+
if (!first)
|
|
13
|
+
return;
|
|
14
|
+
const names = first.kind === "JS_ARRAY_EXPRESSION"
|
|
15
|
+
? (first.children()[0]?.children() ?? [])
|
|
16
|
+
: [first];
|
|
17
|
+
for (const name of names) {
|
|
18
|
+
if (name.kind !== "JS_STRING_LITERAL_EXPRESSION") {
|
|
19
|
+
registerDiagnostic(name, "warning", "Pass require() a string literal module name, not an expression.");
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
});
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { ast, defineRule, registerDiagnostic } from "@biomejs/runtime/plugin";
|
|
2
|
+
import { optionsObject, propertyValue } from "../lib/options.js";
|
|
3
|
+
import { isStaticString } from "../lib/suitescript.js";
|
|
4
|
+
// Matches query.runSuiteQL, runSuiteQLPaged and their .promise forms.
|
|
5
|
+
const SUITEQL = /(^|\.)runSuiteQL(Paged)?(\.promise)?$/;
|
|
6
|
+
// Building SuiteQL from variables invites injection and defeats query plan
|
|
7
|
+
// caching; pass values through `params` placeholders instead. A bare variable
|
|
8
|
+
// is not flagged because its contents are unknown. A call that passes `params`
|
|
9
|
+
// is skipped: the OWASP skill's own examples build `IN (${placeholders})` and an
|
|
10
|
+
// allowlisted `ORDER BY ${column}` into the text and bind the values through
|
|
11
|
+
// `params`.
|
|
12
|
+
export const noDynamicSuiteQL = defineRule({
|
|
13
|
+
query: ast("JS_CALL_EXPRESSION"),
|
|
14
|
+
run(node) {
|
|
15
|
+
if (!SUITEQL.test(node.callee?.text ?? ""))
|
|
16
|
+
return;
|
|
17
|
+
const options = optionsObject(node);
|
|
18
|
+
if (options && propertyValue(options, "params"))
|
|
19
|
+
return;
|
|
20
|
+
const sql = options
|
|
21
|
+
? propertyValue(options, "query")
|
|
22
|
+
: node.arguments?.children()[0]?.children()[0];
|
|
23
|
+
if (sql &&
|
|
24
|
+
!isStaticString(sql) &&
|
|
25
|
+
sql.kind !== "JS_IDENTIFIER_EXPRESSION") {
|
|
26
|
+
registerDiagnostic(sql, "warning", "Build SuiteQL with ? placeholders and params, not string concatenation.");
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
});
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { ast, defineRule, registerDiagnostic } from "@biomejs/runtime/plugin";
|
|
2
|
+
import { ALL_ENTRY_POINTS, GENERIC_ENTRY_POINTS } from "../lib/inference.js";
|
|
3
|
+
import { functionName, scriptSide } from "../lib/script-side.js";
|
|
4
|
+
// Oracle: "Do not deploy scripts with blank entry functions. They still load and
|
|
5
|
+
// may affect performance without doing any work." Only modules whose script type
|
|
6
|
+
// is inferred are checked, so libraries with an empty `get` or `run` are ignored.
|
|
7
|
+
export const noEmptyEntryPoints = defineRule({
|
|
8
|
+
query: ast("JS_FUNCTION_DECLARATION", "JS_FUNCTION_EXPRESSION", "JS_ARROW_FUNCTION_EXPRESSION", "JS_METHOD_OBJECT_MEMBER"),
|
|
9
|
+
run(node) {
|
|
10
|
+
const name = functionName(node);
|
|
11
|
+
if (!name || !ALL_ENTRY_POINTS.has(name) || GENERIC_ENTRY_POINTS.has(name))
|
|
12
|
+
return;
|
|
13
|
+
const body = node.children().find((c) => c.kind === "JS_FUNCTION_BODY");
|
|
14
|
+
const empty = body?.children().every((c) => c.children().length === 0);
|
|
15
|
+
if (!empty || !scriptSide(node))
|
|
16
|
+
return;
|
|
17
|
+
registerDiagnostic(node, "warning", `Entry point '${name}' is empty.`);
|
|
18
|
+
},
|
|
19
|
+
});
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { ast, defineRule, registerDiagnostic } from "@biomejs/runtime/plugin";
|
|
2
|
+
import { parseDefine } from "../lib/define.js";
|
|
3
|
+
// More dependencies than callback parameters: the extra modules load but cannot
|
|
4
|
+
// be used. NetSuite does not fail on it, so this is a warning. The matching
|
|
5
|
+
// check for parameters without a dependency is noExtraParams.
|
|
6
|
+
export const noExtraDeps = defineRule({
|
|
7
|
+
query: ast("JS_CALL_EXPRESSION"),
|
|
8
|
+
run(node) {
|
|
9
|
+
const def = parseDefine(node);
|
|
10
|
+
if (!def)
|
|
11
|
+
return;
|
|
12
|
+
for (const extra of def.elements.slice(def.params.length)) {
|
|
13
|
+
registerDiagnostic(extra, "warning", `Excess module name ${extra.text}.`);
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
});
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { ast, defineRule, registerDiagnostic } from "@biomejs/runtime/plugin";
|
|
2
|
+
import { parseDefine } from "../lib/define.js";
|
|
3
|
+
// More callback parameters than dependencies: the extra parameters are always
|
|
4
|
+
// undefined, which fails when the module uses them. The matching check for
|
|
5
|
+
// dependencies without a parameter is noExtraDeps.
|
|
6
|
+
export const noExtraParams = defineRule({
|
|
7
|
+
query: ast("JS_CALL_EXPRESSION"),
|
|
8
|
+
run(node) {
|
|
9
|
+
const def = parseDefine(node);
|
|
10
|
+
if (!def)
|
|
11
|
+
return;
|
|
12
|
+
for (const extra of def.params.slice(def.elements.length)) {
|
|
13
|
+
if (extra)
|
|
14
|
+
registerDiagnostic(extra, "error", `Excess module identifier '${extra.text}'.`);
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
});
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { ast, defineRule, registerDiagnostic, } from "@biomejs/runtime/plugin";
|
|
2
|
+
import { dependencies } from "../lib/biome-modules.js";
|
|
3
|
+
import { parseDefine } from "../lib/define.js";
|
|
4
|
+
// Oracle's upgrade skill: not renaming `log` or `util` "shadows SS2.1 globals and
|
|
5
|
+
// can break logging or utility calls" (listed as `error` in its SKILL.md).
|
|
6
|
+
// `log` and `util` are SuiteScript 2.1 globals. The one allowed binding is the
|
|
7
|
+
// module they come from: N/log as `log`, N/util as `util`. `error` is not listed
|
|
8
|
+
// because moduleVars names the N/error identifier `error`.
|
|
9
|
+
const GLOBALS = { log: "N/log", util: "N/util" };
|
|
10
|
+
function isModuleBinding(node, module) {
|
|
11
|
+
const ancestors = node.ancestors();
|
|
12
|
+
for (const ancestor of ancestors) {
|
|
13
|
+
if (ancestor.kind === "JS_CALL_EXPRESSION") {
|
|
14
|
+
const def = parseDefine(ancestor);
|
|
15
|
+
if (!def)
|
|
16
|
+
continue;
|
|
17
|
+
const index = def.params.findIndex((p) => p?.text === node.text);
|
|
18
|
+
const isParameter = ancestors.some((a) => a.kind === "JS_FORMAL_PARAMETER");
|
|
19
|
+
const name = def.elements[index]?.text.slice(1, -1);
|
|
20
|
+
return isParameter && index >= 0 && name === module;
|
|
21
|
+
}
|
|
22
|
+
if (ancestor.kind === "JS_IMPORT") {
|
|
23
|
+
return dependencies(ancestor).some((d) => d.name === module);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
export const noGlobalShadowing = defineRule({
|
|
29
|
+
query: ast("JS_IDENTIFIER_BINDING"),
|
|
30
|
+
run(node) {
|
|
31
|
+
const module = Object.hasOwn(GLOBALS, node.text)
|
|
32
|
+
? GLOBALS[node.text]
|
|
33
|
+
: undefined;
|
|
34
|
+
if (module && !isModuleBinding(node, module)) {
|
|
35
|
+
registerDiagnostic(node, "error", `'${node.text}' shadows the global ${node.text}.`);
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
});
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { ast, defineRule, registerDiagnostic } from "@biomejs/runtime/plugin";
|
|
2
|
+
import { optionsObject, propertyValue, stringValue } from "../lib/options.js";
|
|
3
|
+
const PASSWORD_OPTIONS = {
|
|
4
|
+
changeEmail: ["password"],
|
|
5
|
+
changePassword: ["currentPassword", "newPassword"],
|
|
6
|
+
};
|
|
7
|
+
// Oracle (SuiteScript security considerations): "Don't hard-code any passwords in
|
|
8
|
+
// scripts." and, for N/auth, "Be extremely careful when using the N/auth module to
|
|
9
|
+
// change an email or user password. Both the auth.changeEmail(options) and
|
|
10
|
+
// auth.changePassword(options) methods allow you to include a plaintext string for
|
|
11
|
+
// the password."
|
|
12
|
+
export const noHardcodedAuthPasswords = defineRule({
|
|
13
|
+
query: ast("JS_CALL_EXPRESSION"),
|
|
14
|
+
run(node) {
|
|
15
|
+
const match = /^auth\.(changePassword|changeEmail)(\.promise)?$/.exec(node.callee?.text ?? "");
|
|
16
|
+
const keys = match?.[1] && PASSWORD_OPTIONS[match[1]];
|
|
17
|
+
const options = optionsObject(node);
|
|
18
|
+
if (!keys || !options)
|
|
19
|
+
return;
|
|
20
|
+
for (const key of keys) {
|
|
21
|
+
const value = propertyValue(options, key);
|
|
22
|
+
if (value && stringValue(value)) {
|
|
23
|
+
registerDiagnostic(value, "warning", `Do not hard-code the ${key} in a script.`);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
},
|
|
27
|
+
});
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { ast, defineRule, registerDiagnostic } from "@biomejs/runtime/plugin";
|
|
2
|
+
const SECRET_NAMES = /^(password|passwd|pwd|secret|api_?key|client_?secret|token_?secret|consumer_?secret)$/i;
|
|
3
|
+
const unquote = (text) => text.replace(/^['"]|['"]$/g, "");
|
|
4
|
+
// Tier 0: docs/adr/0005-guessing-rules-are-in-no-preset.md.
|
|
5
|
+
// Oracle: "Don't hard-code any passwords in scripts." The OWASP skill agrees, but
|
|
6
|
+
// its `SKILL.md` shows script parameters as the safe place while its
|
|
7
|
+
// references/02 says script parameters are not a secret store and points to API
|
|
8
|
+
// Secrets. `SKILL.md` outranks the reference; the message follows the reference
|
|
9
|
+
// because it is the more precise advice. A heuristic on names, so
|
|
10
|
+
// no preset includes it; enable it explicitly. Only exact secret-like names with
|
|
11
|
+
// a non-empty string literal are flagged (not `passwordId` or `tokenName`).
|
|
12
|
+
export const noHardcodedSecrets = defineRule({
|
|
13
|
+
query: ast("JS_VARIABLE_DECLARATOR", "JS_PROPERTY_OBJECT_MEMBER", "JS_ASSIGNMENT_EXPRESSION"),
|
|
14
|
+
run(node) {
|
|
15
|
+
const children = node.children();
|
|
16
|
+
let name;
|
|
17
|
+
let value;
|
|
18
|
+
if (node.kind === "JS_VARIABLE_DECLARATOR") {
|
|
19
|
+
name = children.find((c) => c.kind === "JS_IDENTIFIER_BINDING")?.text;
|
|
20
|
+
value = children
|
|
21
|
+
.find((c) => c.kind === "JS_INITIALIZER_CLAUSE")
|
|
22
|
+
?.children()[0];
|
|
23
|
+
}
|
|
24
|
+
else if (node.kind === "JS_PROPERTY_OBJECT_MEMBER") {
|
|
25
|
+
name = children[0] && unquote(children[0].text);
|
|
26
|
+
value = children[1];
|
|
27
|
+
}
|
|
28
|
+
else {
|
|
29
|
+
const target = children[0];
|
|
30
|
+
name =
|
|
31
|
+
target?.kind === "JS_STATIC_MEMBER_ASSIGNMENT"
|
|
32
|
+
? target.children()[1]?.text
|
|
33
|
+
: target?.text;
|
|
34
|
+
value = children[1];
|
|
35
|
+
}
|
|
36
|
+
if (name &&
|
|
37
|
+
SECRET_NAMES.test(name) &&
|
|
38
|
+
value?.kind === "JS_STRING_LITERAL_EXPRESSION" &&
|
|
39
|
+
value.text.length > 2) {
|
|
40
|
+
registerDiagnostic(value, "error", `Do not hard-code '${name}'; use an API Secret (https.createSecureString) instead of source code.`);
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
});
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { ast, defineRule, registerDiagnostic } from "@biomejs/runtime/plugin";
|
|
2
|
+
// Oracle: "When you define custom headers, don't include underscores in the name."
|
|
3
|
+
export const noHeaderUnderscores = defineRule({
|
|
4
|
+
query: ast("JS_CALL_EXPRESSION"),
|
|
5
|
+
run(node) {
|
|
6
|
+
if (!/^https\.[a-zA-Z]+(\.promise)?$/.test(node.callee?.text ?? ""))
|
|
7
|
+
return;
|
|
8
|
+
const options = node.arguments?.children()[0]?.children()[0];
|
|
9
|
+
if (options?.kind !== "JS_OBJECT_EXPRESSION")
|
|
10
|
+
return;
|
|
11
|
+
for (const member of options.children()[0]?.children() ?? []) {
|
|
12
|
+
const [name, headers] = member.children();
|
|
13
|
+
if (name?.text !== "headers" || headers?.kind !== "JS_OBJECT_EXPRESSION")
|
|
14
|
+
continue;
|
|
15
|
+
for (const header of headers.children()[0]?.children() ?? []) {
|
|
16
|
+
const key = header.children()[0];
|
|
17
|
+
if (key?.text.replace(/^['"]|['"]$/g, "").includes("_")) {
|
|
18
|
+
registerDiagnostic(key, "warning", "Header names must not contain underscores.");
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
});
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { ast, defineRule, registerDiagnostic } from "@biomejs/runtime/plugin";
|
|
2
|
+
import { callArguments } from "../lib/options.js";
|
|
3
|
+
import { scriptSide } from "../lib/script-side.js";
|
|
4
|
+
import { isStaticString } from "../lib/suitescript.js";
|
|
5
|
+
const SINKS = ["innerHTML", "outerHTML"];
|
|
6
|
+
const CALL_SINKS = /\.(insertAdjacentHTML|write|writeln)$/;
|
|
7
|
+
const SANITIZED = /sanitize|escape|encode|purify/i;
|
|
8
|
+
// Oracle's OWASP skill: "No `innerHTML` with unsanitized content (use
|
|
9
|
+
// `textContent` for untrusted data)", and the same for `document.write`,
|
|
10
|
+
// `outerHTML` and `insertAdjacentHTML`. Its own safe example assigns a static
|
|
11
|
+
// string, so only a value that is not a literal is flagged, and a value wrapped
|
|
12
|
+
// in a call named like a sanitizer is skipped. Server scripts are left to
|
|
13
|
+
// noBrowserGlobals.
|
|
14
|
+
export const noInnerHtml = defineRule({
|
|
15
|
+
query: ast("JS_ASSIGNMENT_EXPRESSION", "JS_CALL_EXPRESSION"),
|
|
16
|
+
run(node) {
|
|
17
|
+
let value;
|
|
18
|
+
if (node.kind === "JS_ASSIGNMENT_EXPRESSION") {
|
|
19
|
+
const [target, assigned] = node.children();
|
|
20
|
+
if (target?.kind === "JS_STATIC_MEMBER_ASSIGNMENT" &&
|
|
21
|
+
SINKS.includes(target.children()[1]?.text ?? ""))
|
|
22
|
+
value = assigned;
|
|
23
|
+
}
|
|
24
|
+
else {
|
|
25
|
+
const callee = node.callee?.text ?? "";
|
|
26
|
+
if (!CALL_SINKS.test(callee))
|
|
27
|
+
return;
|
|
28
|
+
if (/\.(write|writeln)$/.test(callee) && !callee.startsWith("document."))
|
|
29
|
+
return;
|
|
30
|
+
const args = callArguments(node);
|
|
31
|
+
value = callee.endsWith(".insertAdjacentHTML") ? args[1] : args[0];
|
|
32
|
+
}
|
|
33
|
+
if (!value || isStaticString(value))
|
|
34
|
+
return;
|
|
35
|
+
if (value.kind === "JS_CALL_EXPRESSION" && SANITIZED.test(value.text))
|
|
36
|
+
return;
|
|
37
|
+
if (scriptSide(node) === "server")
|
|
38
|
+
return;
|
|
39
|
+
registerDiagnostic(value, "warning", "Do not write unsanitized HTML; use textContent for untrusted data.");
|
|
40
|
+
},
|
|
41
|
+
});
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { ast, defineRule, registerDiagnostic } from "@biomejs/runtime/plugin";
|
|
2
|
+
import { headerCall } from "../lib/headers.js";
|
|
3
|
+
import { isStaticString } from "../lib/suitescript.js";
|
|
4
|
+
// Tier 0: docs/adr/0005-guessing-rules-are-in-no-preset.md.
|
|
5
|
+
// Oracle's OWASP skill: "Cookies set without HttpOnly, Secure, and SameSite
|
|
6
|
+
// attributes are vulnerable to theft via XSS, interception over HTTP, and
|
|
7
|
+
// cross-site request forgery", and Suitelet cookies "must include security
|
|
8
|
+
// attributes". LOW CONFIDENCE, so no preset includes it: only a Set-Cookie value
|
|
9
|
+
// built from literals can be checked, and the skill assumes a Suitelet may set
|
|
10
|
+
// the header at all, which we have not confirmed.
|
|
11
|
+
export const noInsecureCookie = defineRule({
|
|
12
|
+
query: ast("JS_CALL_EXPRESSION"),
|
|
13
|
+
run(node) {
|
|
14
|
+
const header = headerCall(node);
|
|
15
|
+
if (header?.name !== "set-cookie" || !header.value)
|
|
16
|
+
return;
|
|
17
|
+
if (!isStaticString(header.value) &&
|
|
18
|
+
header.value.kind !== "JS_CALL_EXPRESSION")
|
|
19
|
+
return;
|
|
20
|
+
const text = header.value.text;
|
|
21
|
+
if (!/httponly/i.test(text) ||
|
|
22
|
+
!/secure/i.test(text) ||
|
|
23
|
+
!/samesite/i.test(text)) {
|
|
24
|
+
registerDiagnostic(header.value, "warning", "Set-Cookie needs the HttpOnly, Secure and SameSite attributes.");
|
|
25
|
+
}
|
|
26
|
+
},
|
|
27
|
+
});
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { ast, defineRule, registerDiagnostic } from "@biomejs/runtime/plugin";
|
|
2
|
+
// Oracle: the N/https module "does not allow the HTTP protocol".
|
|
3
|
+
export const noInsecureHttpsUrl = defineRule({
|
|
4
|
+
query: ast("JS_CALL_EXPRESSION"),
|
|
5
|
+
run(node) {
|
|
6
|
+
if (!/^https\.[a-zA-Z]+(\.promise)?$/.test(node.callee?.text ?? ""))
|
|
7
|
+
return;
|
|
8
|
+
const options = node.arguments?.children()[0]?.children()[0];
|
|
9
|
+
if (options?.kind !== "JS_OBJECT_EXPRESSION")
|
|
10
|
+
return;
|
|
11
|
+
for (const member of options.children()[0]?.children() ?? []) {
|
|
12
|
+
const [name, value] = member.children();
|
|
13
|
+
if (name?.text === "url" &&
|
|
14
|
+
value?.kind === "JS_STRING_LITERAL_EXPRESSION" &&
|
|
15
|
+
/^['"]http:\/\//i.test(value.text)) {
|
|
16
|
+
registerDiagnostic(value, "error", "N/https does not allow http:// URLs.");
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
});
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { ast, defineRule, registerDiagnostic, } from "@biomejs/runtime/plugin";
|
|
2
|
+
import { callArguments } from "../lib/options.js";
|
|
3
|
+
// A define() call has one of four shapes, and its last argument is the factory:
|
|
4
|
+
// define(factory), define(deps, factory), define(name, factory) or
|
|
5
|
+
// define(name, deps, factory). Arguments that cannot be judged statically
|
|
6
|
+
// (identifiers, calls) are accepted; only a literal of the wrong kind is flagged.
|
|
7
|
+
const NAME = "JS_STRING_LITERAL_EXPRESSION";
|
|
8
|
+
const DEPS = "JS_ARRAY_EXPRESSION";
|
|
9
|
+
const LITERALS = [
|
|
10
|
+
NAME,
|
|
11
|
+
DEPS,
|
|
12
|
+
"JS_NUMBER_LITERAL_EXPRESSION",
|
|
13
|
+
"JS_BOOLEAN_LITERAL_EXPRESSION",
|
|
14
|
+
"JS_NULL_LITERAL_EXPRESSION",
|
|
15
|
+
];
|
|
16
|
+
const wrongLiteral = (node, ...allowed) => !!node && LITERALS.includes(node.kind) && !allowed.includes(node.kind);
|
|
17
|
+
export const noInvalidDefine = defineRule({
|
|
18
|
+
query: ast("JS_CALL_EXPRESSION"),
|
|
19
|
+
run(node) {
|
|
20
|
+
if (node.callee?.text !== "define")
|
|
21
|
+
return;
|
|
22
|
+
const args = callArguments(node);
|
|
23
|
+
const factory = args[args.length - 1];
|
|
24
|
+
let problem;
|
|
25
|
+
if (args.length === 0) {
|
|
26
|
+
problem = "define() needs a factory.";
|
|
27
|
+
}
|
|
28
|
+
else if (args.length > 3) {
|
|
29
|
+
problem =
|
|
30
|
+
"define() takes at most a name, a dependency array and a factory.";
|
|
31
|
+
}
|
|
32
|
+
else if (wrongLiteral(factory)) {
|
|
33
|
+
problem = "The define() factory must be a function or an object.";
|
|
34
|
+
}
|
|
35
|
+
else if (args.length === 2 && wrongLiteral(args[0], NAME, DEPS)) {
|
|
36
|
+
problem =
|
|
37
|
+
"The first define() argument must be a dependency array or a name.";
|
|
38
|
+
}
|
|
39
|
+
else if (args.length === 3 &&
|
|
40
|
+
(wrongLiteral(args[0], NAME) || wrongLiteral(args[1], DEPS))) {
|
|
41
|
+
problem =
|
|
42
|
+
"define(name, dependencies, factory) needs a string name and a dependency array.";
|
|
43
|
+
}
|
|
44
|
+
if (problem)
|
|
45
|
+
registerDiagnostic(node, "error", problem);
|
|
46
|
+
},
|
|
47
|
+
});
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { defineRule, registerDiagnostic, semantic, } from "@biomejs/runtime/plugin";
|
|
2
|
+
import { moduleOfReceiver, typingsOf } from "../lib/module-calls.js";
|
|
3
|
+
// A member the type definitions (Tier 2) do not list for a module enum
|
|
4
|
+
// (`record.Type.CUSTOMR`) evaluates to undefined, which then fails a required
|
|
5
|
+
// option at runtime. The enum's module is resolved from the define() or import
|
|
6
|
+
// binding of the receiver.
|
|
7
|
+
export const noInvalidEnumMember = defineRule({
|
|
8
|
+
query: semantic("JS_STATIC_MEMBER_EXPRESSION"),
|
|
9
|
+
run(node, { model }) {
|
|
10
|
+
const [object, member] = node.children();
|
|
11
|
+
if (object?.kind !== "JS_STATIC_MEMBER_EXPRESSION" || !member)
|
|
12
|
+
return;
|
|
13
|
+
const [receiver, enumName] = object.children();
|
|
14
|
+
if (!receiver || !enumName)
|
|
15
|
+
return;
|
|
16
|
+
const enums = typingsOf(moduleOfReceiver(receiver, model))?.enums;
|
|
17
|
+
const members = enums && Object.hasOwn(enums, enumName.text)
|
|
18
|
+
? enums[enumName.text]
|
|
19
|
+
: undefined;
|
|
20
|
+
if (members && !members.includes(member.text)) {
|
|
21
|
+
registerDiagnostic(member, "warning", `${member.text} is not a member of ${receiver.text}.${enumName.text}.`);
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
});
|