biome-plugin-suitescript 0.2.0 → 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 +148 -77
- package/biome-manifest.json +179 -17
- package/configs/all.json +29 -0
- package/configs/minimal.json +14 -0
- package/configs/recommended.json +20 -0
- 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 +7 -1
- package/dist/lib/options.js +35 -0
- package/dist/lib/required-options.js +14 -0
- package/dist/lib/script-types.js +25 -14
- package/dist/lib/suiteql.js +16 -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/noBlockedHttpsHeader.js +32 -0
- package/dist/rules/noBlockedResponseHeader.js +35 -0
- package/dist/rules/noBrowserGlobals.js +2 -0
- package/dist/rules/noConflictingClientScriptProps.js +36 -0
- package/dist/rules/noDynamicRequire.js +23 -0
- package/dist/rules/noDynamicSuiteQL.js +11 -6
- package/dist/rules/noExtraDeps.js +16 -0
- package/dist/rules/noExtraParams.js +17 -0
- package/dist/rules/noGlobalShadowing.js +6 -2
- package/dist/rules/noHardcodedAuthPasswords.js +27 -0
- package/dist/rules/noHardcodedSecrets.js +7 -2
- package/dist/rules/noInnerHtml.js +41 -0
- package/dist/rules/noInsecureCookie.js +27 -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 -5
- package/dist/rules/noMissingModules.js +6 -3
- package/dist/rules/noModuleScopeApiCalls.js +14 -4
- package/dist/rules/noMultipleDefine.js +14 -0
- package/dist/rules/noNativeIterators.js +8 -5
- package/dist/rules/noPositionalOptions.js +26 -0
- package/dist/rules/noPostMessageWildcard.js +15 -0
- package/dist/rules/noPublicFileUpload.js +20 -0
- package/dist/rules/noRecordInLoop.js +8 -5
- 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 +77 -3
- 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 +3 -1
- package/dist/rules/requireCustpagePrefix.js +14 -13
- 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/noEval.js +0 -21
- package/dist/rules/noExtraModules.js +0 -23
- package/dist/rules/noUserEventContextSave.js +0 -12
|
@@ -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,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
|
+
});
|
|
@@ -8,6 +8,8 @@ const BROWSER_GLOBALS = new Set([
|
|
|
8
8
|
"XMLHttpRequest",
|
|
9
9
|
"alert",
|
|
10
10
|
"navigator",
|
|
11
|
+
"URLSearchParams",
|
|
12
|
+
"fetch",
|
|
11
13
|
]);
|
|
12
14
|
// Server scripts run outside a browser, but TypeScript's DOM library lets
|
|
13
15
|
// these names compile. Locals that reuse a name resolve to a binding and are left alone.
|
|
@@ -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
|
+
});
|
|
@@ -1,20 +1,25 @@
|
|
|
1
1
|
import { ast, defineRule, registerDiagnostic } from "@biomejs/runtime/plugin";
|
|
2
|
+
import { optionsObject, propertyValue } from "../lib/options.js";
|
|
2
3
|
import { isStaticString } from "../lib/suitescript.js";
|
|
3
4
|
// Matches query.runSuiteQL, runSuiteQLPaged and their .promise forms.
|
|
4
5
|
const SUITEQL = /(^|\.)runSuiteQL(Paged)?(\.promise)?$/;
|
|
5
6
|
// Building SuiteQL from variables invites injection and defeats query plan
|
|
6
7
|
// caching; pass values through `params` placeholders instead. A bare variable
|
|
7
|
-
// is not flagged because its contents are unknown.
|
|
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`.
|
|
8
12
|
export const noDynamicSuiteQL = defineRule({
|
|
9
13
|
query: ast("JS_CALL_EXPRESSION"),
|
|
10
14
|
run(node) {
|
|
11
15
|
if (!SUITEQL.test(node.callee?.text ?? ""))
|
|
12
16
|
return;
|
|
13
|
-
|
|
14
|
-
if (
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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];
|
|
18
23
|
if (sql &&
|
|
19
24
|
!isStaticString(sql) &&
|
|
20
25
|
sql.kind !== "JS_IDENTIFIER_EXPRESSION") {
|
|
@@ -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
|
+
});
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { ast, defineRule, registerDiagnostic, } from "@biomejs/runtime/plugin";
|
|
2
2
|
import { dependencies } from "../lib/biome-modules.js";
|
|
3
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).
|
|
4
6
|
// `log` and `util` are SuiteScript 2.1 globals. The one allowed binding is the
|
|
5
7
|
// module they come from: N/log as `log`, N/util as `util`. `error` is not listed
|
|
6
8
|
// because moduleVars names the N/error identifier `error`.
|
|
@@ -26,9 +28,11 @@ function isModuleBinding(node, module) {
|
|
|
26
28
|
export const noGlobalShadowing = defineRule({
|
|
27
29
|
query: ast("JS_IDENTIFIER_BINDING"),
|
|
28
30
|
run(node) {
|
|
29
|
-
const module = GLOBALS
|
|
31
|
+
const module = Object.hasOwn(GLOBALS, node.text)
|
|
32
|
+
? GLOBALS[node.text]
|
|
33
|
+
: undefined;
|
|
30
34
|
if (module && !isModuleBinding(node, module)) {
|
|
31
|
-
registerDiagnostic(node, "
|
|
35
|
+
registerDiagnostic(node, "error", `'${node.text}' shadows the global ${node.text}.`);
|
|
32
36
|
}
|
|
33
37
|
},
|
|
34
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
|
+
});
|
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
import { ast, defineRule, registerDiagnostic } from "@biomejs/runtime/plugin";
|
|
2
2
|
const SECRET_NAMES = /^(password|passwd|pwd|secret|api_?key|client_?secret|token_?secret|consumer_?secret)$/i;
|
|
3
3
|
const unquote = (text) => text.replace(/^['"]|['"]$/g, "");
|
|
4
|
-
//
|
|
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
|
|
5
10
|
// no preset includes it; enable it explicitly. Only exact secret-like names with
|
|
6
11
|
// a non-empty string literal are flagged (not `passwordId` or `tokenName`).
|
|
7
12
|
export const noHardcodedSecrets = defineRule({
|
|
@@ -32,7 +37,7 @@ export const noHardcodedSecrets = defineRule({
|
|
|
32
37
|
SECRET_NAMES.test(name) &&
|
|
33
38
|
value?.kind === "JS_STRING_LITERAL_EXPRESSION" &&
|
|
34
39
|
value.text.length > 2) {
|
|
35
|
-
registerDiagnostic(value, "error", `Do not hard-code '${name}';
|
|
40
|
+
registerDiagnostic(value, "error", `Do not hard-code '${name}'; use an API Secret (https.createSecureString) instead of source code.`);
|
|
36
41
|
}
|
|
37
42
|
},
|
|
38
43
|
});
|
|
@@ -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,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
|
+
});
|
|
@@ -0,0 +1,20 @@
|
|
|
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: "SAFE: use the documented redirect module instead of
|
|
6
|
+
// writing raw headers", because an unvalidated value in a response header can
|
|
7
|
+
// split the response. LOW CONFIDENCE, so no preset includes it: the value may
|
|
8
|
+
// have been validated, and the skill's references still set Location after
|
|
9
|
+
// sanitising it.
|
|
10
|
+
export const noLocationHeader = defineRule({
|
|
11
|
+
query: ast("JS_CALL_EXPRESSION"),
|
|
12
|
+
run(node) {
|
|
13
|
+
const header = headerCall(node);
|
|
14
|
+
if (header?.name === "location" &&
|
|
15
|
+
header.value &&
|
|
16
|
+
!isStaticString(header.value)) {
|
|
17
|
+
registerDiagnostic(header.value, "warning", "Use N/redirect instead of writing a Location header from a variable.");
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
});
|
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
import { ast, defineRule, registerDiagnostic } from "@biomejs/runtime/plugin";
|
|
2
2
|
import { dependencies } from "../lib/biome-modules.js";
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
3
|
+
// Tier 0: docs/adr/0005-guessing-rules-are-in-no-preset.md, docs/adr/0006-log-is-a-global-with-two-opposite-styles.md.
|
|
4
|
+
// Opt-in style: use the global `log` instead of loading N/log. Oracle's help
|
|
5
|
+
// center says "NetSuite loads the log Object by default for all script types. You
|
|
6
|
+
// do not need to load it manually", which is what this rule follows. It is the
|
|
7
|
+
// opposite of requireLogModule, so enable only one of them. (The upgrade skill
|
|
8
|
+
// says the global exists but loading is recommended, which requireLogModule
|
|
9
|
+
// follows; the learning skill says it is not global and loses.) Logging in a client script attached to a form is
|
|
10
|
+
// ignored either way; see noClientLog.
|
|
8
11
|
export const noLogModule = defineRule({
|
|
9
12
|
query: ast("JS_CALL_EXPRESSION", "JS_IMPORT"),
|
|
10
13
|
run(node) {
|
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
import { defineRule, registerDiagnostic, semantic, } from "@biomejs/runtime/plugin";
|
|
2
2
|
import { MODULES } from "../lib/modules.js";
|
|
3
3
|
import { inSuiteScriptModule } from "../lib/suitescript.js";
|
|
4
|
-
//
|
|
5
|
-
//
|
|
4
|
+
// Tier 0: docs/adr/0006-log-is-a-global-with-two-opposite-styles.md.
|
|
5
|
+
// `log` and `util` are globals in SuiteScript 2.1 ("NetSuite loads the log Object
|
|
6
|
+
// by default for all script types. You do not need to load it manually."), so
|
|
7
|
+
// neither needs loading. requireLogModule is the style that wants N/log loaded.
|
|
8
|
+
const GLOBALS = ["log", "util"];
|
|
6
9
|
const ALIASES = new Map(Object.entries(MODULES)
|
|
7
|
-
.filter(([, identifier]) => identifier
|
|
10
|
+
.filter(([, identifier]) => !GLOBALS.includes(identifier))
|
|
8
11
|
.map(([name, identifier]) => [identifier, name]));
|
|
9
12
|
// Reports `record.load()` when `record` resolves to no binding, meaning the
|
|
10
13
|
// module was never added to `define()` or imported. Locals that shadow the
|
|
@@ -7,8 +7,16 @@ const FUNCTIONS = [
|
|
|
7
7
|
"JS_METHOD_OBJECT_MEMBER",
|
|
8
8
|
"JS_METHOD_CLASS_MEMBER",
|
|
9
9
|
];
|
|
10
|
-
|
|
11
|
-
//
|
|
10
|
+
const FACTORIES = /\.(createColumn|createFilter|createSort|createSetting)$/;
|
|
11
|
+
// Oracle's SAFE guide: "All N/ API modules are unavailable during the `define()`
|
|
12
|
+
// callback execution. Calling `runtime.getCurrentScript()`, `record.load()`, or
|
|
13
|
+
// any N/ API at module scope causes deployment failure." Initialize lazily from an
|
|
14
|
+
// entry point instead. N/log is exempt because log is a global. The failure claim
|
|
15
|
+
// comes only from this skill and has not been checked against Oracle's
|
|
16
|
+
// documentation, and its examples are calls that do work (getCurrentScript, record.load).
|
|
17
|
+
// Calls that only build an object (`search.createColumn`, `createFilter`,
|
|
18
|
+
// `createSort`, `createSetting`) are skipped, because nothing says they fail; remove
|
|
19
|
+
// them from FACTORIES if that turns out to be wrong.
|
|
12
20
|
export const noModuleScopeApiCalls = defineRule({
|
|
13
21
|
query: ast("JS_CALL_EXPRESSION"),
|
|
14
22
|
run(node) {
|
|
@@ -26,8 +34,10 @@ export const noModuleScopeApiCalls = defineRule({
|
|
|
26
34
|
const object = callee?.kind === "JS_STATIC_MEMBER_EXPRESSION"
|
|
27
35
|
? callee.children()[0]?.text
|
|
28
36
|
: undefined;
|
|
29
|
-
if (object &&
|
|
30
|
-
|
|
37
|
+
if (object &&
|
|
38
|
+
aliases.has(object) &&
|
|
39
|
+
!FACTORIES.test(callee?.text ?? "")) {
|
|
40
|
+
registerDiagnostic(n, "error", `${callee?.text}() runs at module scope; call it from an entry point.`);
|
|
31
41
|
}
|
|
32
42
|
}
|
|
33
43
|
for (const child of n.children())
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { ast, defineRule, registerDiagnostic } from "@biomejs/runtime/plugin";
|
|
2
|
+
import { descendants } from "../lib/script-side.js";
|
|
3
|
+
// Oracle (entry point script validation): "The script must include one, and only
|
|
4
|
+
// one, properly structured define statement. The script can't include multiple
|
|
5
|
+
// define statements." NetSuite rejects the upload with MULTIPLE_DEFINE_CALLS.
|
|
6
|
+
export const noMultipleDefine = defineRule({
|
|
7
|
+
query: ast("JS_MODULE", "JS_SCRIPT"),
|
|
8
|
+
run(node) {
|
|
9
|
+
const defines = [...descendants(node)].filter((n) => n.kind === "JS_CALL_EXPRESSION" && n.callee?.text === "define");
|
|
10
|
+
for (const extra of defines.slice(1)) {
|
|
11
|
+
registerDiagnostic(extra, "error", "A script can call define() only once.");
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
});
|
|
@@ -1,16 +1,19 @@
|
|
|
1
1
|
import { ast, defineRule, registerDiagnostic } from "@biomejs/runtime/plugin";
|
|
2
|
-
//
|
|
3
|
-
//
|
|
2
|
+
// Oracle's SAFE guide: "`function*` generators and `Symbol.iterator` usage passes
|
|
3
|
+
// Jest/Node.js but fails in GraalJS; use standard array methods instead." Its own
|
|
4
|
+
// samples use `for...of`, so that is not flagged. Generator methods (`*foo() {}`)
|
|
5
|
+
// are generators too. The failure claim comes only from this skill and has not been
|
|
6
|
+
// checked against Oracle's documentation.
|
|
4
7
|
export const noNativeIterators = defineRule({
|
|
5
|
-
query: ast("JS_FUNCTION_DECLARATION", "JS_FUNCTION_EXPRESSION", "JS_STATIC_MEMBER_EXPRESSION"),
|
|
8
|
+
query: ast("JS_FUNCTION_DECLARATION", "JS_FUNCTION_EXPRESSION", "JS_METHOD_OBJECT_MEMBER", "JS_METHOD_CLASS_MEMBER", "JS_STATIC_MEMBER_EXPRESSION"),
|
|
6
9
|
run(node) {
|
|
7
10
|
if (node.kind === "JS_STATIC_MEMBER_EXPRESSION") {
|
|
8
11
|
if (node.text === "Symbol.iterator") {
|
|
9
|
-
registerDiagnostic(node, "
|
|
12
|
+
registerDiagnostic(node, "error", "Avoid Symbol.iterator.");
|
|
10
13
|
}
|
|
11
14
|
}
|
|
12
15
|
else if (node.starToken) {
|
|
13
|
-
registerDiagnostic(node, "
|
|
16
|
+
registerDiagnostic(node, "error", "Avoid generator functions.");
|
|
14
17
|
}
|
|
15
18
|
},
|
|
16
19
|
});
|