@objectstack/lint 14.3.0 → 14.6.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/dist/index.cjs +61 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +10 -2
- package/dist/index.d.ts +10 -2
- package/dist/index.js +58 -1
- package/dist/index.js.map +1 -1
- package/package.json +5 -5
package/dist/index.cjs
CHANGED
|
@@ -35,7 +35,10 @@ __export(index_exports, {
|
|
|
35
35
|
PAGE_SOURCE_CLASSNAME: () => PAGE_SOURCE_CLASSNAME,
|
|
36
36
|
SECURITY_ANCHOR_HIGH_PRIVILEGE: () => SECURITY_ANCHOR_HIGH_PRIVILEGE,
|
|
37
37
|
SECURITY_BOOK_AUDIENCE_UNKNOWN_SET: () => SECURITY_BOOK_AUDIENCE_UNKNOWN_SET,
|
|
38
|
+
SECURITY_DELEGATION_MISSING_REASON: () => SECURITY_DELEGATION_MISSING_REASON,
|
|
38
39
|
SECURITY_EXTERNAL_WIDER: () => SECURITY_EXTERNAL_WIDER,
|
|
40
|
+
SECURITY_GRANT_EXPIRED_AT_AUTHORING: () => SECURITY_GRANT_EXPIRED_AT_AUTHORING,
|
|
41
|
+
SECURITY_MASTER_DETAIL_UNGRANTED: () => SECURITY_MASTER_DETAIL_UNGRANTED,
|
|
39
42
|
SECURITY_OWD_ALIAS: () => SECURITY_OWD_ALIAS,
|
|
40
43
|
SECURITY_OWD_UNSET: () => SECURITY_OWD_UNSET,
|
|
41
44
|
SECURITY_PRIVATE_NO_READSCOPE: () => SECURITY_PRIVATE_NO_READSCOPE,
|
|
@@ -1467,6 +1470,9 @@ var SECURITY_ROLE_WORD = "security-role-word";
|
|
|
1467
1470
|
var SECURITY_BOOK_AUDIENCE_UNKNOWN_SET = "security-book-audience-unknown-set";
|
|
1468
1471
|
var SECURITY_PRIVATE_NO_READSCOPE = "security-private-no-readscope";
|
|
1469
1472
|
var SECURITY_MASTER_DETAIL_UNGRANTED = "security-master-detail-ungranted";
|
|
1473
|
+
var SECURITY_FLS_UNQUALIFIED_KEY = "security-fls-unqualified-key";
|
|
1474
|
+
var SECURITY_GRANT_EXPIRED_AT_AUTHORING = "security-grant-expired-at-authoring";
|
|
1475
|
+
var SECURITY_DELEGATION_MISSING_REASON = "security-delegation-missing-reason";
|
|
1470
1476
|
var CANONICAL_OWD = ["private", "public_read", "public_read_write", "controlled_by_parent"];
|
|
1471
1477
|
var OWD_ALIAS_FIX = {
|
|
1472
1478
|
read: "public_read",
|
|
@@ -1515,7 +1521,7 @@ function firstMasterDetailField(obj) {
|
|
|
1515
1521
|
function grantsObjectAccess(p) {
|
|
1516
1522
|
return p.allowRead === true || p.allowCreate === true || p.allowEdit === true || p.allowDelete === true || p.viewAllRecords === true || p.modifyAllRecords === true;
|
|
1517
1523
|
}
|
|
1518
|
-
function validateSecurityPosture(stack) {
|
|
1524
|
+
function validateSecurityPosture(stack, opts) {
|
|
1519
1525
|
const findings = [];
|
|
1520
1526
|
if (!stack || typeof stack !== "object") return findings;
|
|
1521
1527
|
const objects = asArray14(stack.objects);
|
|
@@ -1585,6 +1591,18 @@ function validateSecurityPosture(stack) {
|
|
|
1585
1591
|
const psName = typeof ps.name === "string" ? ps.name : `(permission set ${i})`;
|
|
1586
1592
|
const psPath = `permissions[${i}]`;
|
|
1587
1593
|
const objectsMap = ps.objects && typeof ps.objects === "object" ? ps.objects : {};
|
|
1594
|
+
const flsMap = ps.fields && typeof ps.fields === "object" ? ps.fields : {};
|
|
1595
|
+
for (const flsKey of Object.keys(flsMap)) {
|
|
1596
|
+
if (flsKey.includes(".")) continue;
|
|
1597
|
+
findings.push({
|
|
1598
|
+
severity: "error",
|
|
1599
|
+
rule: SECURITY_FLS_UNQUALIFIED_KEY,
|
|
1600
|
+
where: `permission set "${psName}"`,
|
|
1601
|
+
path: `${psPath}.fields["${flsKey}"]`,
|
|
1602
|
+
message: `field-permission key '${flsKey}' is not object-qualified \u2014 the runtime matches FLS keys by '<object>.<field>' prefix, so a bare key is silently IGNORED and the declared masking never enforces.`,
|
|
1603
|
+
hint: `Qualify the key with its object, e.g. 'crm_opportunity.${flsKey}': { readable: true, editable: false }.`
|
|
1604
|
+
});
|
|
1605
|
+
}
|
|
1588
1606
|
const wildcard = objectsMap["*"];
|
|
1589
1607
|
if (wildcard && (wildcard.viewAllRecords === true || wildcard.modifyAllRecords === true)) {
|
|
1590
1608
|
findings.push({
|
|
@@ -1736,6 +1754,45 @@ function validateSecurityPosture(stack) {
|
|
|
1736
1754
|
}
|
|
1737
1755
|
}
|
|
1738
1756
|
}
|
|
1757
|
+
const GRANT_SEED_OBJECTS = /* @__PURE__ */ new Set(["sys_user_position", "sys_user_permission_set"]);
|
|
1758
|
+
const nowMs = opts?.nowMs ?? Date.now();
|
|
1759
|
+
for (const [i, seed] of asArray14(stack.data).entries()) {
|
|
1760
|
+
const seedObject = typeof seed.object === "string" ? seed.object : "";
|
|
1761
|
+
if (!GRANT_SEED_OBJECTS.has(seedObject)) continue;
|
|
1762
|
+
const records = Array.isArray(seed.records) ? seed.records : [];
|
|
1763
|
+
for (let j = 0; j < records.length; j++) {
|
|
1764
|
+
const rec = records[j] ?? {};
|
|
1765
|
+
const where = `seed "${seedObject}" record #${j}`;
|
|
1766
|
+
const until = rec.valid_until;
|
|
1767
|
+
if (until != null && until !== "") {
|
|
1768
|
+
const ms = typeof until === "number" ? until < 1e12 ? until * 1e3 : until : until instanceof Date ? until.getTime() : typeof until === "string" ? Date.parse(until) : Number.NaN;
|
|
1769
|
+
if (Number.isNaN(ms) || ms <= nowMs) {
|
|
1770
|
+
findings.push({
|
|
1771
|
+
severity: "error",
|
|
1772
|
+
rule: SECURITY_GRANT_EXPIRED_AT_AUTHORING,
|
|
1773
|
+
where,
|
|
1774
|
+
path: `data[${i}].records[${j}].valid_until`,
|
|
1775
|
+
message: Number.isNaN(ms) ? `valid_until ${JSON.stringify(until)} is not a parseable timestamp \u2014 the resolver fails closed (ADR-0091 D2), so this grant will NEVER be active.` : `valid_until ${JSON.stringify(until)} is already in the past \u2014 this grant is expired at authoring time and will never resolve (ADR-0091 D2 filters it fail-closed).`,
|
|
1776
|
+
hint: `Set valid_until to a future instant (ISO-8601 UTC), or drop the column for an unbounded grant. If the row is a historical record, it belongs in audit history, not seed data.`
|
|
1777
|
+
});
|
|
1778
|
+
}
|
|
1779
|
+
}
|
|
1780
|
+
const delegatedFrom = rec.delegated_from;
|
|
1781
|
+
if (delegatedFrom != null && delegatedFrom !== "") {
|
|
1782
|
+
const reason = rec.reason;
|
|
1783
|
+
if (typeof reason !== "string" || reason.trim().length === 0) {
|
|
1784
|
+
findings.push({
|
|
1785
|
+
severity: "error",
|
|
1786
|
+
rule: SECURITY_DELEGATION_MISSING_REASON,
|
|
1787
|
+
where,
|
|
1788
|
+
path: `data[${i}].records[${j}].reason`,
|
|
1789
|
+
message: `delegation row (delegated_from = ${JSON.stringify(delegatedFrom)}) has no reason. ADR-0091 D3 requires a mandatory reason on every delegation for the dual audit trail (granted_by = writer, delegated_from = authority source, reason = why).`,
|
|
1790
|
+
hint: `Add reason: 'vacation stand-in for \u5F20\u4E09, 2026-08-01..15' (free text, required).`
|
|
1791
|
+
});
|
|
1792
|
+
}
|
|
1793
|
+
}
|
|
1794
|
+
}
|
|
1795
|
+
}
|
|
1739
1796
|
return findings;
|
|
1740
1797
|
}
|
|
1741
1798
|
|
|
@@ -1844,7 +1901,10 @@ function diffAccessMatrix(before, after) {
|
|
|
1844
1901
|
PAGE_SOURCE_CLASSNAME,
|
|
1845
1902
|
SECURITY_ANCHOR_HIGH_PRIVILEGE,
|
|
1846
1903
|
SECURITY_BOOK_AUDIENCE_UNKNOWN_SET,
|
|
1904
|
+
SECURITY_DELEGATION_MISSING_REASON,
|
|
1847
1905
|
SECURITY_EXTERNAL_WIDER,
|
|
1906
|
+
SECURITY_GRANT_EXPIRED_AT_AUTHORING,
|
|
1907
|
+
SECURITY_MASTER_DETAIL_UNGRANTED,
|
|
1848
1908
|
SECURITY_OWD_ALIAS,
|
|
1849
1909
|
SECURITY_OWD_UNSET,
|
|
1850
1910
|
SECURITY_PRIVATE_NO_READSCOPE,
|