biome-plugin-suitescript 0.2.0 → 0.4.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.
Files changed (69) hide show
  1. package/README.md +154 -78
  2. package/biome-manifest.json +179 -17
  3. package/configs/all.json +29 -0
  4. package/configs/minimal.json +14 -0
  5. package/configs/recommended.json +20 -0
  6. package/dist/lib/define.js +35 -5
  7. package/dist/lib/headers.js +12 -0
  8. package/dist/lib/module-calls.js +65 -0
  9. package/dist/lib/module-script-types.js +7 -1
  10. package/dist/lib/options.js +35 -0
  11. package/dist/lib/required-options.js +14 -0
  12. package/dist/lib/script-types.js +25 -14
  13. package/dist/lib/suiteql.js +16 -0
  14. package/dist/lib/typings-data.js +4327 -0
  15. package/dist/lib/typings.js +2 -0
  16. package/dist/rules/logRequireDetails.js +4 -0
  17. package/dist/rules/logRequireTitle.js +1 -0
  18. package/dist/rules/noAuthorizationHeaderInInternalRequest.js +24 -0
  19. package/dist/rules/noBlockedHttpsHeader.js +32 -0
  20. package/dist/rules/noBlockedResponseHeader.js +35 -0
  21. package/dist/rules/noBrowserGlobals.js +2 -0
  22. package/dist/rules/noConflictingClientScriptProps.js +36 -0
  23. package/dist/rules/noDynamicRequire.js +23 -0
  24. package/dist/rules/noDynamicSuiteQL.js +11 -6
  25. package/dist/rules/noExtraDeps.js +16 -0
  26. package/dist/rules/noExtraParams.js +17 -0
  27. package/dist/rules/noGlobalShadowing.js +6 -2
  28. package/dist/rules/noHardcodedAuthPasswords.js +27 -0
  29. package/dist/rules/noHardcodedSecrets.js +7 -2
  30. package/dist/rules/noInnerHtml.js +41 -0
  31. package/dist/rules/noInsecureCookie.js +27 -0
  32. package/dist/rules/noInvalidDefine.js +47 -0
  33. package/dist/rules/noInvalidEnumMember.js +24 -0
  34. package/dist/rules/noLocationHeader.js +20 -0
  35. package/dist/rules/noLogModule.js +8 -5
  36. package/dist/rules/noMissingModules.js +6 -3
  37. package/dist/rules/noModuleScopeApiCalls.js +14 -4
  38. package/dist/rules/noMultipleDefine.js +14 -0
  39. package/dist/rules/noNativeIterators.js +8 -5
  40. package/dist/rules/noPositionalOptions.js +26 -0
  41. package/dist/rules/noPostMessageWildcard.js +15 -0
  42. package/dist/rules/noPublicFileUpload.js +20 -0
  43. package/dist/rules/noRecordInLoop.js +8 -5
  44. package/dist/rules/noRequireInsteadOfDefine.js +25 -0
  45. package/dist/rules/noSecretKeyGuidAndSecret.js +16 -0
  46. package/dist/rules/noSensitiveBrowserStorage.js +24 -0
  47. package/dist/rules/noStackInResponse.js +21 -0
  48. package/dist/rules/noSuiteQLSelectStar.js +18 -0
  49. package/dist/rules/noSuiteQLSystemNoteJoin.js +19 -0
  50. package/dist/rules/noSuiteQLTypeInSearch.js +41 -0
  51. package/dist/rules/noSuiteScript1Api.js +77 -3
  52. package/dist/rules/noUnknownModuleMember.js +23 -0
  53. package/dist/rules/noUnknownOptions.js +31 -0
  54. package/dist/rules/noUnsupportedServerWidgetFieldType.js +40 -0
  55. package/dist/rules/noWeakHash.js +17 -0
  56. package/dist/rules/noWildcardCors.js +16 -0
  57. package/dist/rules/noWrongScriptTypeModules.js +3 -1
  58. package/dist/rules/requireCustpagePrefix.js +14 -13
  59. package/dist/rules/requireEachReturn.js +38 -0
  60. package/dist/rules/requireLogModule.js +25 -0
  61. package/dist/rules/requireOptions.js +26 -0
  62. package/dist/rules/requireOrderByForPagedSuiteQL.js +17 -0
  63. package/dist/rules/requireQueryOptions.js +53 -0
  64. package/dist/rules/requireServerResponseOptions.js +33 -0
  65. package/dist/rules/requireServerWidgetOptions.js +37 -0
  66. package/package.json +10 -1
  67. package/dist/rules/noEval.js +0 -21
  68. package/dist/rules/noExtraModules.js +0 -23
  69. package/dist/rules/noUserEventContextSave.js +0 -12
@@ -0,0 +1,2 @@
1
+ // The shape of the data generated from @hitc/netsuite-types (see typings-data.ts).
2
+ export {};
@@ -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
- let sql = node.arguments?.children()[0]?.children()[0];
14
- if (sql?.kind === "JS_OBJECT_EXPRESSION") {
15
- const member = (sql.children()[0]?.children() ?? []).find((m) => m.children()[0]?.text.replace(/^['"]|['"]$/g, "") === "query");
16
- sql = member?.children()[1];
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[node.text];
31
+ const module = Object.hasOwn(GLOBALS, node.text)
32
+ ? GLOBALS[node.text]
33
+ : undefined;
30
34
  if (module && !isModuleBinding(node, module)) {
31
- registerDiagnostic(node, "warning", `'${node.text}' shadows the global ${node.text}.`);
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
- // Oracle: "Do not hard-code any passwords in scripts." A heuristic on names, so
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}'; read it from a script parameter or credentials.`);
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
- // Opt-in style: use the global `log` instead of loading N/log. It is the opposite
4
- // of noMissingModules, which requires loading N/log, so enable only one of them.
5
- // Oracle's skills disagree on this (upgrade: the global exists but importing is
6
- // recommended; learning: it is not global). The global is not available in
7
- // client scripts, so scope this rule away from them with the plugin `includes`.
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
- // `util` is a global in SuiteScript 2.1, so it never needs loading. `log` is
5
- // treated like every other module: load N/log when you call it.
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 !== "util")
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
- // N/ APIs called while the define() callback runs (module scope) can fail on
11
- // deploy; initialize lazily from an entry point instead. N/log is exempt.
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 && aliases.has(object)) {
30
- registerDiagnostic(n, "warning", `${callee?.text}() runs at module scope; call it from an entry point.`);
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
- // NetSuite runs GraalJS; Oracle's guidance is to use array methods, not
3
- // generators or Symbol.iterator.
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, "warning", "Avoid Symbol.iterator.");
12
+ registerDiagnostic(node, "error", "Avoid Symbol.iterator.");
10
13
  }
11
14
  }
12
15
  else if (node.starToken) {
13
- registerDiagnostic(node, "warning", "Avoid generator functions.");
16
+ registerDiagnostic(node, "error", "Avoid generator functions.");
14
17
  }
15
18
  },
16
19
  });