@lunora/advisor 1.0.0-alpha.33 → 1.0.0-alpha.35
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.d.mts +61 -1
- package/dist/index.d.ts +61 -1
- package/dist/index.mjs +1 -200
- package/dist/packem_shared/AE_METRIC_EVENTS-LP_0D7Uw.mjs +1 -0
- package/dist/packem_shared/actionFetchSsrf-BbWxy1Yf.mjs +1 -0
- package/dist/packem_shared/adminRouteWithoutGuard-DSo4t9j4.mjs +1 -0
- package/dist/packem_shared/aiRawRunEscapeHatch-DfhjYm43.mjs +1 -0
- package/dist/packem_shared/aiToolSideEffectPromptInjection-DnWQOoKi.mjs +1 -0
- package/dist/packem_shared/aiUnboundedGenerationPublic-D3HZrh5_.mjs +1 -0
- package/dist/packem_shared/allowUnauthenticatedShardAccessEnabled-D4aMNBb0.mjs +1 -0
- package/dist/packem_shared/argument-derived-sink-BpO3NPPM.mjs +1 -0
- package/dist/packem_shared/authApiCallWithoutHeaders-BItMMdZP.mjs +1 -0
- package/dist/packem_shared/authCsrfCheckDisabled-De9ammtV.mjs +1 -0
- package/dist/packem_shared/authEmailVerificationDisabled-BFKdJcx4.mjs +1 -0
- package/dist/packem_shared/authSecureCookiesDisabled-B4WIzvMm.mjs +1 -0
- package/dist/packem_shared/authSessionFreshageZero-Bpcaf_Pe.mjs +1 -0
- package/dist/packem_shared/authTrustedOriginsWildcard-DjU9NXEm.mjs +1 -0
- package/dist/packem_shared/browserAllowPrivateTargets-Dxyeh6lg.mjs +1 -0
- package/dist/packem_shared/browserUserUrlWithoutAllowlist-BHBS6b0N.mjs +1 -0
- package/dist/packem_shared/circularFk-BMuJKmuZ.mjs +1 -0
- package/dist/packem_shared/constraintValidator-N339mfyx.mjs +1 -0
- package/dist/packem_shared/containerInstanceKeyFromUserInput-C8S4T3ty.mjs +1 -0
- package/dist/packem_shared/containerOversizedInstance-B9wW_sU5.mjs +1 -0
- package/dist/packem_shared/containerPublicInternet-Bx2J7tha.mjs +1 -0
- package/dist/packem_shared/containerRuntimeEgressRelaxation-BQ2ZD1aV.mjs +1 -0
- package/dist/packem_shared/containerStartEnableInternetOverride-B62WUura.mjs +1 -0
- package/dist/packem_shared/dedupeCacheKeys-7JgxaQWV.mjs +1 -0
- package/dist/packem_shared/duplicateIndex-DFT7qLQD.mjs +1 -0
- package/dist/packem_shared/emptyIndex-BiXEfyfN.mjs +1 -0
- package/dist/packem_shared/exportSinkMisconfigured-DvpAaa6j.mjs +1 -0
- package/dist/packem_shared/externalSourceIncrementalNoDeletePath-DnoL64YY.mjs +1 -0
- package/dist/packem_shared/externalSourceOnGlobal-DNPLoNUg.mjs +1 -0
- package/dist/packem_shared/externalSourceUnscoped-DqBp5jYr.mjs +1 -0
- package/dist/packem_shared/filterWithoutIndex-ht2GZQWV.mjs +1 -0
- package/dist/packem_shared/finding-BAVgqnCF.mjs +1 -0
- package/dist/packem_shared/fk-index-Bmk19RvQ.mjs +1 -0
- package/dist/packem_shared/flagGatesSecurityWithUnsafeDefault-DPGFEHTt.mjs +1 -0
- package/dist/packem_shared/fromServerSchema-CJ-AQ2kw.mjs +1 -0
- package/dist/packem_shared/geoIndexFieldNotGeopoint-DAGLZtaH.mjs +1 -0
- package/dist/packem_shared/geoIndexUnused-BgDE6_AR.mjs +1 -0
- package/dist/packem_shared/hardcodedSecret-rIRc3aNk.mjs +1 -0
- package/dist/packem_shared/helpers-DwlvEUCh.mjs +1 -0
- package/dist/packem_shared/hotShard-DVs_U17R.mjs +1 -0
- package/dist/packem_shared/httpActionMissingAuthGuard-BZDdeW_D.mjs +1 -0
- package/dist/packem_shared/httpActionResponseHeaderInjection-D063rB7U.mjs +1 -0
- package/dist/packem_shared/hyperdriveOutsideAction-CRmOQz67.mjs +1 -0
- package/dist/packem_shared/identityUndeclaredClaimTrusted-BYRYGQ74.mjs +1 -0
- package/dist/packem_shared/imagesUrlSourceFromUserInput-Cy5pc_dA.mjs +1 -0
- package/dist/packem_shared/indexReferencesUnknownField-r3QHlaz6.mjs +1 -0
- package/dist/packem_shared/indexUtilization-QfbIH_w4.mjs +1 -0
- package/dist/packem_shared/insertManyUnsafeUserData-DrH7PdRs.mjs +1 -0
- package/dist/packem_shared/kvUnscopedUserKeyIdor-B4lGxRxh.mjs +1 -0
- package/dist/packem_shared/mailInboundDispatchWithoutVerify-BivYJ5F9.mjs +1 -0
- package/dist/packem_shared/mailRecipientFromRequestInput-BR_oSh65.mjs +1 -0
- package/dist/packem_shared/maskUncoveredPiiColumn-DqPfZM2O.mjs +1 -0
- package/dist/packem_shared/maskWeakHashStrategyOnPii-CEiiGgT7.mjs +1 -0
- package/dist/packem_shared/maskedRelationLeakViaWith-CxYk0MDW.mjs +1 -0
- package/dist/packem_shared/mutatorFullRowReplace-CEgecSGm.mjs +1 -0
- package/dist/packem_shared/nondeterministicQueryMutation-BaMfbAk_.mjs +1 -0
- package/dist/packem_shared/normalizeIdUsedAsAuthorization-FjISSXJm.mjs +1 -0
- package/dist/packem_shared/notifyMissingPushConfig-C_GbVw3S.mjs +1 -0
- package/dist/packem_shared/notifySendOutsideAction-B0jZuUP3.mjs +1 -0
- package/dist/packem_shared/outputProjectionMissingOnPublicRead-9Pm9766s.mjs +1 -0
- package/dist/packem_shared/ownerFieldFromArgsNotAuth-BoR9vvst.mjs +1 -0
- package/dist/packem_shared/paymentCreateWithoutAuthorize-CurtqN8K.mjs +1 -0
- package/dist/packem_shared/paymentWebhookWideTolerance-I7ingfR8.mjs +1 -0
- package/dist/packem_shared/plaintextSecretInWranglerVariables-DXjKIm8H.mjs +1 -0
- package/dist/packem_shared/policyReferencesUnknownTable-D3WelAQa.mjs +1 -0
- package/dist/packem_shared/privilegedDispatchUnvalidatedPayload-74GuGye1.mjs +1 -0
- package/dist/packem_shared/privilegedFanoutFromPublicProcedure-C2j2GSUN.mjs +1 -0
- package/dist/packem_shared/publicArgumentUsesAny-BJb4cTqL.mjs +1 -0
- package/dist/packem_shared/publicMutationWithoutRatelimit-D9phluHW.mjs +1 -0
- package/dist/packem_shared/publicTableRlsOptoutConfusion-CaWtPGAn.mjs +1 -0
- package/dist/packem_shared/queueWithoutDlq-BLY3iy9Q.mjs +1 -0
- package/dist/packem_shared/r2sqlOutsideAction-CKK_I1Ms.mjs +1 -0
- package/dist/packem_shared/ratelimitDefaultMemoryStore-mOdI3Jmb.mjs +1 -0
- package/dist/packem_shared/ratelimitKeySpoofableOrGlobal-DaGId_Fh.mjs +1 -0
- package/dist/packem_shared/ratelimitMiddlewareFailOpen-CTWVosEc.mjs +1 -0
- package/dist/packem_shared/relationReferencesUnknownField-clqyB69Y.mjs +1 -0
- package/dist/packem_shared/relationReferencesUnknownTable-CLdrhkmS.mjs +1 -0
- package/dist/packem_shared/rlsUncoveredTable-DYHgnoos.mjs +1 -0
- package/dist/packem_shared/shapeTargetsGlobalTable-Blz-aSkg.mjs +1 -0
- package/dist/packem_shared/shapeUnknownTable-DFxtyFio.mjs +1 -0
- package/dist/packem_shared/signupMutationWithoutDisposableGating-3ir808Vz.mjs +1 -0
- package/dist/packem_shared/softDeleteIncludeDeletedFromArgs-DgxpQHRA.mjs +1 -0
- package/dist/packem_shared/sqlInjectionRisk-D2NaT-5c.mjs +1 -0
- package/dist/packem_shared/storageGenerateUploadUrlNoContentTypePin-C9cjl6Jh.mjs +1 -0
- package/dist/packem_shared/storageKeyFromUserArgs-BeS5cp56.mjs +1 -0
- package/dist/packem_shared/storagePresignedUrlForPrivateContent-j7VhwIdu.mjs +1 -0
- package/dist/packem_shared/storageUploadWithoutContentTypeAllowlist-oxKLecuf.mjs +1 -0
- package/dist/packem_shared/storageUploadWithoutMaxSize-DUDxPutk.mjs +1 -0
- package/dist/packem_shared/tableWithoutInsert-D3AhaNzR.mjs +1 -0
- package/dist/packem_shared/ttlFieldNotTimestamp-BFiRkSZD.mjs +1 -0
- package/dist/packem_shared/unboundedStringArgument-AkkIcr-S.mjs +1 -0
- package/dist/packem_shared/unindexedForeignKey-DK4eWXkb.mjs +1 -0
- package/dist/packem_shared/unindexedRelationTarget-B5fFq1E8.mjs +1 -0
- package/dist/packem_shared/unrestrictedWhereBranch-YKiydie4.mjs +1 -0
- package/dist/packem_shared/userCreatingMutationWithoutCaptcha-iDjRkUFO.mjs +1 -0
- package/dist/packem_shared/vectorsNamespaceFromUserInput-Cyq5B2kF.mjs +1 -0
- package/dist/packem_shared/workflowDuplicateStepName-cXF6Rpvg.mjs +1 -0
- package/dist/packem_shared/workflowUnknownTarget-JF8rITLP.mjs +1 -0
- package/dist/packem_shared/workflowUnused-Dx_FQwGE.mjs +1 -0
- package/package.json +3 -3
- package/dist/packem_shared/AE_METRIC_EVENTS-BM14d0lm.mjs +0 -87
- package/dist/packem_shared/actionFetchSsrf-wbmYgzmz.mjs +0 -26
- package/dist/packem_shared/adminRouteWithoutGuard-UUGBkAjU.mjs +0 -33
- package/dist/packem_shared/aiRawRunEscapeHatch-C29jd32J.mjs +0 -26
- package/dist/packem_shared/aiToolSideEffectPromptInjection-BtD3alqB.mjs +0 -33
- package/dist/packem_shared/aiUnboundedGenerationPublic-CbAcSxLB.mjs +0 -26
- package/dist/packem_shared/allowUnauthenticatedShardAccessEnabled-BcASq-jP.mjs +0 -30
- package/dist/packem_shared/argument-derived-sink-C1xTTqAt.mjs +0 -30
- package/dist/packem_shared/authApiCallWithoutHeaders-BeJhCZaf.mjs +0 -38
- package/dist/packem_shared/authCsrfCheckDisabled-DCf9FSoD.mjs +0 -26
- package/dist/packem_shared/authEmailVerificationDisabled-Dk-Tc5oU.mjs +0 -26
- package/dist/packem_shared/authSecureCookiesDisabled-osJrHW9Y.mjs +0 -26
- package/dist/packem_shared/authSessionFreshageZero-CjufCc_Q.mjs +0 -26
- package/dist/packem_shared/authTrustedOriginsWildcard-ylv4PKzC.mjs +0 -26
- package/dist/packem_shared/browserAllowPrivateTargets-CBvQxLAM.mjs +0 -26
- package/dist/packem_shared/browserUserUrlWithoutAllowlist-CvhA6w59.mjs +0 -26
- package/dist/packem_shared/circularFk-CNcAVuYa.mjs +0 -127
- package/dist/packem_shared/constraintValidator-CxwtpJ6E.mjs +0 -206
- package/dist/packem_shared/containerInstanceKeyFromUserInput-BUXj2J4w.mjs +0 -19
- package/dist/packem_shared/containerOversizedInstance-5U1VKPRM.mjs +0 -36
- package/dist/packem_shared/containerPublicInternet-CuNerJE5.mjs +0 -30
- package/dist/packem_shared/containerRuntimeEgressRelaxation-B0LlfmDA.mjs +0 -26
- package/dist/packem_shared/containerStartEnableInternetOverride-DDaHZQ1L.mjs +0 -26
- package/dist/packem_shared/dedupeCacheKeys-r5B7u_yq.mjs +0 -13
- package/dist/packem_shared/duplicateIndex-BOublMSt.mjs +0 -57
- package/dist/packem_shared/emptyIndex-BX8EuEY7.mjs +0 -32
- package/dist/packem_shared/exportSinkMisconfigured-897sVMUa.mjs +0 -46
- package/dist/packem_shared/externalSourceIncrementalNoDeletePath-DvX9cRBD.mjs +0 -45
- package/dist/packem_shared/externalSourceOnGlobal-Bg-NfCX9.mjs +0 -30
- package/dist/packem_shared/externalSourceUnscoped-5vT-Bup3.mjs +0 -44
- package/dist/packem_shared/filterWithoutIndex-BYVeJaSs.mjs +0 -31
- package/dist/packem_shared/finding-Dm_zvzS1.mjs +0 -16
- package/dist/packem_shared/fk-index-IUK1ukgs.mjs +0 -7
- package/dist/packem_shared/flagGatesSecurityWithUnsafeDefault-BhIs0shr.mjs +0 -41
- package/dist/packem_shared/fromServerSchema-CVnbSf8j.mjs +0 -70
- package/dist/packem_shared/geoIndexFieldNotGeopoint-CcaInLhY.mjs +0 -40
- package/dist/packem_shared/geoIndexUnused-D7I0UyND.mjs +0 -40
- package/dist/packem_shared/hardcodedSecret-Be-pKVdn.mjs +0 -31
- package/dist/packem_shared/helpers-BySnKhVB.mjs +0 -31
- package/dist/packem_shared/hotShard-CkC7qpre.mjs +0 -60
- package/dist/packem_shared/httpActionMissingAuthGuard-CxipddNx.mjs +0 -35
- package/dist/packem_shared/httpActionResponseHeaderInjection-DOFS7pFT.mjs +0 -39
- package/dist/packem_shared/hyperdriveOutsideAction-BgZqX7Xg.mjs +0 -30
- package/dist/packem_shared/identityUndeclaredClaimTrusted-D8nXV2dd.mjs +0 -32
- package/dist/packem_shared/imagesUrlSourceFromUserInput-YcDQ0b_Y.mjs +0 -19
- package/dist/packem_shared/indexReferencesUnknownField-BSWNngxX.mjs +0 -36
- package/dist/packem_shared/indexUtilization-B5DMQ3bI.mjs +0 -45
- package/dist/packem_shared/insertManyUnsafeUserData-Dn77XpmX.mjs +0 -26
- package/dist/packem_shared/kvUnscopedUserKeyIdor-YWwmfE8X.mjs +0 -19
- package/dist/packem_shared/mailInboundDispatchWithoutVerify-CHSRA8zz.mjs +0 -28
- package/dist/packem_shared/mailRecipientFromRequestInput-Cka2qu5J.mjs +0 -26
- package/dist/packem_shared/maskUncoveredPiiColumn-DjGIPG6M.mjs +0 -61
- package/dist/packem_shared/maskWeakHashStrategyOnPii-1c4q8Opf.mjs +0 -35
- package/dist/packem_shared/maskedRelationLeakViaWith-CPI4s0sl.mjs +0 -76
- package/dist/packem_shared/mutatorFullRowReplace-BJnNDaIV.mjs +0 -26
- package/dist/packem_shared/nondeterministicQueryMutation-GXES1fLp.mjs +0 -35
- package/dist/packem_shared/normalizeIdUsedAsAuthorization-BVPtCpzT.mjs +0 -50
- package/dist/packem_shared/notifyMissingPushConfig-Ca9EzRzy.mjs +0 -27
- package/dist/packem_shared/notifySendOutsideAction-BUZSIZa4.mjs +0 -30
- package/dist/packem_shared/outputProjectionMissingOnPublicRead-Bl5IMx0k.mjs +0 -51
- package/dist/packem_shared/ownerFieldFromArgsNotAuth-mOw3hE5z.mjs +0 -26
- package/dist/packem_shared/paymentCreateWithoutAuthorize-BYm4JLxo.mjs +0 -26
- package/dist/packem_shared/paymentWebhookWideTolerance-D9QQJMJq.mjs +0 -35
- package/dist/packem_shared/plaintextSecretInWranglerVariables-NKO4YkKf.mjs +0 -35
- package/dist/packem_shared/policyReferencesUnknownTable-DtaIEovd.mjs +0 -38
- package/dist/packem_shared/privilegedDispatchUnvalidatedPayload-5Forjckt.mjs +0 -37
- package/dist/packem_shared/privilegedFanoutFromPublicProcedure-D3dL01B8.mjs +0 -26
- package/dist/packem_shared/publicArgumentUsesAny-C71b2NCf.mjs +0 -32
- package/dist/packem_shared/publicMutationWithoutRatelimit-DbIhgi7j.mjs +0 -36
- package/dist/packem_shared/publicTableRlsOptoutConfusion-Bn71yoD4.mjs +0 -38
- package/dist/packem_shared/queueWithoutDlq-CSkGNb_0.mjs +0 -41
- package/dist/packem_shared/r2sqlOutsideAction-CtqxvMuV.mjs +0 -30
- package/dist/packem_shared/ratelimitDefaultMemoryStore-BISChG5C.mjs +0 -26
- package/dist/packem_shared/ratelimitKeySpoofableOrGlobal-DqlHYQQ3.mjs +0 -26
- package/dist/packem_shared/ratelimitMiddlewareFailOpen-CJgDCaUw.mjs +0 -33
- package/dist/packem_shared/relationReferencesUnknownField-CjbLScJ1.mjs +0 -54
- package/dist/packem_shared/relationReferencesUnknownTable-DrorpKYe.mjs +0 -33
- package/dist/packem_shared/rlsUncoveredTable-CxEfZ5eZ.mjs +0 -56
- package/dist/packem_shared/shapeTargetsGlobalTable-DHrf4Koi.mjs +0 -34
- package/dist/packem_shared/shapeUnknownTable-C8aDWFoe.mjs +0 -34
- package/dist/packem_shared/signupMutationWithoutDisposableGating-Dnbol2CS.mjs +0 -39
- package/dist/packem_shared/softDeleteIncludeDeletedFromArgs-BLqDKrkM.mjs +0 -38
- package/dist/packem_shared/sqlInjectionRisk-zwytYGLt.mjs +0 -26
- package/dist/packem_shared/storageGenerateUploadUrlNoContentTypePin-Da4L9Ge8.mjs +0 -26
- package/dist/packem_shared/storageKeyFromUserArgs-B86elJgS.mjs +0 -19
- package/dist/packem_shared/storagePresignedUrlForPrivateContent-yGmb8uqz.mjs +0 -47
- package/dist/packem_shared/storageUploadWithoutContentTypeAllowlist-BV-gF1lT.mjs +0 -27
- package/dist/packem_shared/storageUploadWithoutMaxSize-DpxO59wU.mjs +0 -27
- package/dist/packem_shared/tableWithoutInsert-CbbaYIP4.mjs +0 -34
- package/dist/packem_shared/ttlFieldNotTimestamp-Dpaj63Z6.mjs +0 -36
- package/dist/packem_shared/unboundedStringArgument-DThg2-wt.mjs +0 -32
- package/dist/packem_shared/unindexedForeignKey-BgJbKyqK.mjs +0 -45
- package/dist/packem_shared/unindexedRelationTarget-D6eyj6Xx.mjs +0 -53
- package/dist/packem_shared/userCreatingMutationWithoutCaptcha-2DZtWIPb.mjs +0 -42
- package/dist/packem_shared/vectorsNamespaceFromUserInput-CQhr5bVn.mjs +0 -19
- package/dist/packem_shared/workflowDuplicateStepName-ioBxPBCy.mjs +0 -48
- package/dist/packem_shared/workflowUnknownTarget-Cdd7WhKQ.mjs +0 -34
- package/dist/packem_shared/workflowUnused-D0jHxdz9.mjs +0 -38
|
@@ -1,206 +0,0 @@
|
|
|
1
|
-
import { e as emit } from './finding-Dm_zvzS1.mjs';
|
|
2
|
-
|
|
3
|
-
const MAX_EXAMPLE_IDS = 5;
|
|
4
|
-
const rowId = (row) => typeof row["_id"] === "string" ? row["_id"] : "?";
|
|
5
|
-
const truncatedSuffix = (capped, cap, phrase) => capped ? ` (sample capped at ${cap.toString()} rows — ${phrase})` : "";
|
|
6
|
-
const formatExamples = (ids, total) => {
|
|
7
|
-
const examples = ids.slice(0, MAX_EXAMPLE_IDS);
|
|
8
|
-
const more = total - examples.length;
|
|
9
|
-
const list = examples.map((id) => `"${id}"`).join(", ");
|
|
10
|
-
return more > 0 ? `${list} (+${more.toString()} more)` : list;
|
|
11
|
-
};
|
|
12
|
-
const resolveReferenceValues = (referencedColumn, targetSample) => {
|
|
13
|
-
if (referencedColumn === "_id") {
|
|
14
|
-
return targetSample.existingIds;
|
|
15
|
-
}
|
|
16
|
-
const values = /* @__PURE__ */ new Set();
|
|
17
|
-
for (const row of targetSample.rows) {
|
|
18
|
-
const value = row[referencedColumn];
|
|
19
|
-
if (value !== null && value !== void 0) {
|
|
20
|
-
values.add(String(value));
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
return values;
|
|
24
|
-
};
|
|
25
|
-
const checkFkRelation = (lint, sample, relation, targetSample) => {
|
|
26
|
-
if (relation.kind !== "one") {
|
|
27
|
-
return void 0;
|
|
28
|
-
}
|
|
29
|
-
if (targetSample.truncated) {
|
|
30
|
-
return void 0;
|
|
31
|
-
}
|
|
32
|
-
const referencedColumn = relation.references;
|
|
33
|
-
const referenceValues = resolveReferenceValues(referencedColumn, targetSample);
|
|
34
|
-
const fkColumn = relation.field;
|
|
35
|
-
const danglingIds = [];
|
|
36
|
-
for (const row of sample.rows) {
|
|
37
|
-
const fkValue = row[fkColumn];
|
|
38
|
-
if (fkValue === null || fkValue === void 0) {
|
|
39
|
-
continue;
|
|
40
|
-
}
|
|
41
|
-
const fkString = String(fkValue);
|
|
42
|
-
if (!referenceValues.has(fkString)) {
|
|
43
|
-
danglingIds.push(rowId(row));
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
if (danglingIds.length === 0) {
|
|
47
|
-
return void 0;
|
|
48
|
-
}
|
|
49
|
-
const isTruncated = sample.truncated;
|
|
50
|
-
const { cap } = sample;
|
|
51
|
-
const referencedLabel = referencedColumn === "_id" ? `"${relation.table}"` : `"${relation.table}"."${referencedColumn}"`;
|
|
52
|
-
const detail = `Table "${sample.table}": ${danglingIds.length.toString()} sampled row(s) have a dangling FK value in column "${fkColumn}" — the referenced ${referencedLabel} row does not exist. Row ids: ${formatExamples(danglingIds, danglingIds.length)}${truncatedSuffix(isTruncated, cap, "more rows may exist beyond the window")}.`;
|
|
53
|
-
return emit(lint, {
|
|
54
|
-
cacheKey: `constraint_validator:fk:${sample.table}:${fkColumn}`,
|
|
55
|
-
detail,
|
|
56
|
-
metadata: {
|
|
57
|
-
cap,
|
|
58
|
-
column: fkColumn,
|
|
59
|
-
count: danglingIds.length,
|
|
60
|
-
examples: danglingIds.slice(0, MAX_EXAMPLE_IDS),
|
|
61
|
-
kind: "fk",
|
|
62
|
-
references: referencedColumn,
|
|
63
|
-
referencesTable: relation.table,
|
|
64
|
-
table: sample.table,
|
|
65
|
-
truncated: isTruncated
|
|
66
|
-
}
|
|
67
|
-
});
|
|
68
|
-
};
|
|
69
|
-
const checkNullField = (lint, sample, field, isRequired) => {
|
|
70
|
-
if (!isRequired) {
|
|
71
|
-
return void 0;
|
|
72
|
-
}
|
|
73
|
-
const nullIds = [];
|
|
74
|
-
for (const row of sample.rows) {
|
|
75
|
-
const value = row[field];
|
|
76
|
-
if (value === null || value === void 0) {
|
|
77
|
-
nullIds.push(rowId(row));
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
if (nullIds.length === 0) {
|
|
81
|
-
return void 0;
|
|
82
|
-
}
|
|
83
|
-
const detail = `Table "${sample.table}": ${nullIds.length.toString()} sampled row(s) have a null/missing value in column "${field}", which is declared as non-optional. Row ids: ${formatExamples(nullIds, nullIds.length)}${truncatedSuffix(sample.truncated, sample.cap, "more rows may exist beyond the window")}.`;
|
|
84
|
-
return emit(lint, {
|
|
85
|
-
cacheKey: `constraint_validator:null:${sample.table}:${field}`,
|
|
86
|
-
detail,
|
|
87
|
-
metadata: {
|
|
88
|
-
cap: sample.cap,
|
|
89
|
-
column: field,
|
|
90
|
-
count: nullIds.length,
|
|
91
|
-
examples: nullIds.slice(0, MAX_EXAMPLE_IDS),
|
|
92
|
-
kind: "null",
|
|
93
|
-
table: sample.table,
|
|
94
|
-
truncated: sample.truncated
|
|
95
|
-
}
|
|
96
|
-
});
|
|
97
|
-
};
|
|
98
|
-
const compositeKey = (row, fields) => {
|
|
99
|
-
const parts = [];
|
|
100
|
-
for (const field of fields) {
|
|
101
|
-
const value = row[field];
|
|
102
|
-
if (value === null || value === void 0) {
|
|
103
|
-
return void 0;
|
|
104
|
-
}
|
|
105
|
-
parts.push(JSON.stringify(value));
|
|
106
|
-
}
|
|
107
|
-
return parts.join("|");
|
|
108
|
-
};
|
|
109
|
-
const recordDuplicate = (id, existingId, duplicateIds) => {
|
|
110
|
-
if (!duplicateIds.includes(existingId)) {
|
|
111
|
-
duplicateIds.push(existingId);
|
|
112
|
-
}
|
|
113
|
-
duplicateIds.push(id);
|
|
114
|
-
};
|
|
115
|
-
const checkUniqueIndex = (lint, sample, index) => {
|
|
116
|
-
if (index.kind !== "index" || index.unique !== true) {
|
|
117
|
-
return void 0;
|
|
118
|
-
}
|
|
119
|
-
const seen = /* @__PURE__ */ new Map();
|
|
120
|
-
const duplicateIds = [];
|
|
121
|
-
for (const row of sample.rows) {
|
|
122
|
-
const id = rowId(row);
|
|
123
|
-
const key = compositeKey(row, index.fields);
|
|
124
|
-
if (key === void 0) {
|
|
125
|
-
continue;
|
|
126
|
-
}
|
|
127
|
-
const existingId = seen.get(key);
|
|
128
|
-
if (existingId === void 0) {
|
|
129
|
-
seen.set(key, id);
|
|
130
|
-
} else {
|
|
131
|
-
recordDuplicate(id, existingId, duplicateIds);
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
if (duplicateIds.length === 0) {
|
|
135
|
-
return void 0;
|
|
136
|
-
}
|
|
137
|
-
const detail = `Table "${sample.table}": ${duplicateIds.length.toString()} sampled row(s) share duplicate values on unique index "${index.name}" (${index.fields.join(", ")}). Row ids: ${formatExamples(duplicateIds, duplicateIds.length)}${truncatedSuffix(sample.truncated, sample.cap, "duplicates beyond the window may exist")}.`;
|
|
138
|
-
return emit(lint, {
|
|
139
|
-
cacheKey: `constraint_validator:unique:${sample.table}:${index.name}`,
|
|
140
|
-
detail,
|
|
141
|
-
metadata: {
|
|
142
|
-
cap: sample.cap,
|
|
143
|
-
count: duplicateIds.length,
|
|
144
|
-
examples: duplicateIds.slice(0, MAX_EXAMPLE_IDS),
|
|
145
|
-
fields: index.fields,
|
|
146
|
-
index: index.name,
|
|
147
|
-
kind: "unique",
|
|
148
|
-
table: sample.table,
|
|
149
|
-
truncated: sample.truncated
|
|
150
|
-
}
|
|
151
|
-
});
|
|
152
|
-
};
|
|
153
|
-
const checkTable = (lint, sample, sampleByTable, tableByName) => {
|
|
154
|
-
const schemaTable = tableByName.get(sample.table);
|
|
155
|
-
if (!schemaTable) {
|
|
156
|
-
return [];
|
|
157
|
-
}
|
|
158
|
-
const findings = [];
|
|
159
|
-
for (const relation of schemaTable.relations) {
|
|
160
|
-
const targetSample = sampleByTable.get(relation.table);
|
|
161
|
-
if (!targetSample) {
|
|
162
|
-
continue;
|
|
163
|
-
}
|
|
164
|
-
const f = checkFkRelation(lint, sample, relation, targetSample);
|
|
165
|
-
if (f) {
|
|
166
|
-
findings.push(f);
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
for (const field of schemaTable.fields) {
|
|
170
|
-
const isRequired = !schemaTable.optionalFields?.has(field);
|
|
171
|
-
const f = checkNullField(lint, sample, field, isRequired);
|
|
172
|
-
if (f) {
|
|
173
|
-
findings.push(f);
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
for (const index of schemaTable.indexes) {
|
|
177
|
-
const f = checkUniqueIndex(lint, sample, index);
|
|
178
|
-
if (f) {
|
|
179
|
-
findings.push(f);
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
return findings;
|
|
183
|
-
};
|
|
184
|
-
const constraintValidator = {
|
|
185
|
-
categories: ["SCHEMA"],
|
|
186
|
-
description: "Sampled rows violate one or more declared constraints: a foreign-key column references a non-existent row, a non-optional column contains null, or a unique-indexed column has duplicate values. These violations indicate data inserted before a constraint was enforced (e.g. a schema migration, a raw import, or a bug in a past data-migration transform).",
|
|
187
|
-
facing: "INTERNAL",
|
|
188
|
-
level: "WARN",
|
|
189
|
-
name: "constraint_validator",
|
|
190
|
-
remediation: "Inspect the listed rows and correct or remove the violating values. Run a data migration to backfill nulls, dedup unique violations, or re-link dangling FK references. For FK violations, confirm the target table rows exist before fixing the referencing rows.",
|
|
191
|
-
run: (context) => {
|
|
192
|
-
if (!context.tableSamples || context.tableSamples.length === 0) {
|
|
193
|
-
return [];
|
|
194
|
-
}
|
|
195
|
-
const sampleByTable = /* @__PURE__ */ new Map();
|
|
196
|
-
for (const sample of context.tableSamples) {
|
|
197
|
-
sampleByTable.set(sample.table, sample);
|
|
198
|
-
}
|
|
199
|
-
const tableByName = new Map(context.schema.tables.map((table) => [table.name, table]));
|
|
200
|
-
return context.tableSamples.flatMap((sample) => checkTable(constraintValidator, sample, sampleByTable, tableByName));
|
|
201
|
-
},
|
|
202
|
-
source: "runtime",
|
|
203
|
-
title: "Constraint violation"
|
|
204
|
-
};
|
|
205
|
-
|
|
206
|
-
export { constraintValidator as default };
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
import { m as makeArgumentDerivedSinkLint } from './argument-derived-sink-C1xTTqAt.mjs';
|
|
2
|
-
|
|
3
|
-
const containerInstanceKeyFromUserInput = makeArgumentDerivedSinkLint({
|
|
4
|
-
cacheKey: (access) => `container_instance_key_from_user_input:${access.file}:${access.line.toString()}`,
|
|
5
|
-
categories: ["SECURITY"],
|
|
6
|
-
description: "A `ctx.containers.<name>.get` call routes to a container instance using a key derived from the handler's `args` with no server-side scoping. Any caller can supply another tenant's key and reach that tenant's container instance — a cross-tenant insecure direct object reference (IDOR).",
|
|
7
|
-
detail: (access) => `\`ctx.containers.*.${access.method}\` in \`${access.exportName}\` (${access.file}:${access.line.toString()}) routes to a container instance using a key derived from \`args\` with no server-side scoping — any caller can reach another tenant's container (IDOR). Derive the key from a server-trusted identity (e.g. \`\${ctx.auth.userId}\`).`,
|
|
8
|
-
facing: "EXTERNAL",
|
|
9
|
-
getAccesses: (context) => context.containerKeyAccesses,
|
|
10
|
-
level: "WARN",
|
|
11
|
-
metadata: (access) => {
|
|
12
|
-
return { exportName: access.exportName, file: access.file, line: access.line, method: access.method };
|
|
13
|
-
},
|
|
14
|
-
name: "container_instance_key_from_user_input",
|
|
15
|
-
remediation: `Derive the container instance key from a server-trusted identity (e.g. \`\${ctx.auth.userId}\`) or a record the caller owns — never pass request input straight to \`ctx.containers.<name>.get\`.`,
|
|
16
|
-
title: "Possible cross-tenant IDOR from arg-derived unscoped container key"
|
|
17
|
-
});
|
|
18
|
-
|
|
19
|
-
export { containerInstanceKeyFromUserInput as default };
|
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
import { e as emit } from './finding-Dm_zvzS1.mjs';
|
|
2
|
-
|
|
3
|
-
const LARGE_NAMED = /* @__PURE__ */ new Set(["standard-3", "standard-4"]);
|
|
4
|
-
const VCPU_THRESHOLD = 2;
|
|
5
|
-
const MEMORY_MIB_THRESHOLD = 4096;
|
|
6
|
-
const isOversized = (instanceType) => typeof instanceType === "string" ? LARGE_NAMED.has(instanceType) : (instanceType.vcpu ?? 0) > VCPU_THRESHOLD || (instanceType.memoryMib ?? 0) > MEMORY_MIB_THRESHOLD;
|
|
7
|
-
const containerOversizedInstance = {
|
|
8
|
-
categories: ["PERFORMANCE"],
|
|
9
|
-
description: "A container is declared on a large instance type, whose provisioned memory and disk are billed for the lifetime of every running instance.",
|
|
10
|
-
facing: "INTERNAL",
|
|
11
|
-
level: "INFO",
|
|
12
|
-
name: "container_oversized_instance",
|
|
13
|
-
remediation: "Confirm the workload needs this size; a smaller instanceType (lite/basic/standard-1) costs less while idle-but-running.",
|
|
14
|
-
run: (context) => {
|
|
15
|
-
const findings = [];
|
|
16
|
-
for (const container of context.containers ?? []) {
|
|
17
|
-
const { instanceType } = container;
|
|
18
|
-
if (instanceType === void 0 || !isOversized(instanceType)) {
|
|
19
|
-
continue;
|
|
20
|
-
}
|
|
21
|
-
const size = typeof instanceType === "string" ? instanceType : JSON.stringify(instanceType);
|
|
22
|
-
findings.push(
|
|
23
|
-
emit(containerOversizedInstance, {
|
|
24
|
-
cacheKey: `container_oversized_instance:${container.exportName}`,
|
|
25
|
-
detail: `Container "${container.exportName}" uses a large instance type (${size}); its memory + disk are billed while any instance runs.`,
|
|
26
|
-
metadata: { container: container.exportName, instanceType }
|
|
27
|
-
})
|
|
28
|
-
);
|
|
29
|
-
}
|
|
30
|
-
return findings;
|
|
31
|
-
},
|
|
32
|
-
source: "static",
|
|
33
|
-
title: "Oversized container instance"
|
|
34
|
-
};
|
|
35
|
-
|
|
36
|
-
export { containerOversizedInstance as default };
|
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
import { e as emit } from './finding-Dm_zvzS1.mjs';
|
|
2
|
-
|
|
3
|
-
const containerPublicInternet = {
|
|
4
|
-
categories: ["SECURITY"],
|
|
5
|
-
description: "A container leaves outbound internet access at the default (enabled), which is billed per GB of egress and widens the attack surface.",
|
|
6
|
-
facing: "INTERNAL",
|
|
7
|
-
level: "INFO",
|
|
8
|
-
name: "container_public_internet",
|
|
9
|
-
remediation: "Set `enableInternet: false` on the container if it doesn't call external services, or `true` to opt in deliberately.",
|
|
10
|
-
run: (context) => {
|
|
11
|
-
const findings = [];
|
|
12
|
-
for (const container of context.containers ?? []) {
|
|
13
|
-
if (container.enableInternet !== void 0) {
|
|
14
|
-
continue;
|
|
15
|
-
}
|
|
16
|
-
findings.push(
|
|
17
|
-
emit(containerPublicInternet, {
|
|
18
|
-
cacheKey: `container_public_internet:${container.exportName}`,
|
|
19
|
-
detail: `Container "${container.exportName}" doesn't set enableInternet, so outbound internet is on by default (egress is billed).`,
|
|
20
|
-
metadata: { container: container.exportName }
|
|
21
|
-
})
|
|
22
|
-
);
|
|
23
|
-
}
|
|
24
|
-
return findings;
|
|
25
|
-
},
|
|
26
|
-
source: "static",
|
|
27
|
-
title: "Container egress enabled by default"
|
|
28
|
-
};
|
|
29
|
-
|
|
30
|
-
export { containerPublicInternet as default };
|
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
import { e as emit } from './finding-Dm_zvzS1.mjs';
|
|
2
|
-
|
|
3
|
-
const containerRuntimeEgressRelaxation = {
|
|
4
|
-
categories: ["SECURITY"],
|
|
5
|
-
description: "A runtime `.egress.allow(...)` / `.egress.deny(...)` / `.egress.setAllowed(...)` call mutates the container's egress firewall at runtime, re-opening a network surface the static analysis assumes is locked to the `defineContainer` declaration.",
|
|
6
|
-
facing: "EXTERNAL",
|
|
7
|
-
level: "WARN",
|
|
8
|
-
name: "container_runtime_egress_relaxation",
|
|
9
|
-
remediation: "Prefer declaring the egress policy statically in `lunora/containers.ts` where it is auditable. When a runtime mutation is unavoidable, scope it behind a server-trusted check and keep the change narrow (a specific host, not a broad allow) rather than an unconditional relaxation.",
|
|
10
|
-
run: (context) => {
|
|
11
|
-
if (context.containerOverrides === void 0) {
|
|
12
|
-
return [];
|
|
13
|
-
}
|
|
14
|
-
return context.containerOverrides.filter((override) => override.kind === "egress_relaxation").map(
|
|
15
|
-
(override) => emit(containerRuntimeEgressRelaxation, {
|
|
16
|
-
cacheKey: `container_runtime_egress_relaxation:${override.file}:${override.line.toString()}`,
|
|
17
|
-
detail: `\`${override.exportName}\` calls \`.egress.${override.detail}(...)\` at runtime, mutating the container's egress firewall.`,
|
|
18
|
-
metadata: { exportName: override.exportName, file: override.file, line: override.line, method: override.detail }
|
|
19
|
-
})
|
|
20
|
-
);
|
|
21
|
-
},
|
|
22
|
-
source: "static",
|
|
23
|
-
title: "Runtime egress mutation relaxes the container firewall"
|
|
24
|
-
};
|
|
25
|
-
|
|
26
|
-
export { containerRuntimeEgressRelaxation as default };
|
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
import { e as emit } from './finding-Dm_zvzS1.mjs';
|
|
2
|
-
|
|
3
|
-
const containerStartEnableInternetOverride = {
|
|
4
|
-
categories: ["SECURITY"],
|
|
5
|
-
description: "A runtime `.start({ enableInternet: true, … })` call overrides the static `defineContainer` declaration and re-opens public internet egress for that container instance, defeating the `container_public_internet` lint's assumption that the declaration is authoritative.",
|
|
6
|
-
facing: "EXTERNAL",
|
|
7
|
-
level: "WARN",
|
|
8
|
-
name: "container_start_enable_internet_override",
|
|
9
|
-
remediation: "Avoid overriding `enableInternet` at `.start()` time. Set it once on the `defineContainer` declaration in `lunora/containers.ts` so the static advisors can reason about it, and gate any runtime opt-in behind a server-trusted check rather than an unconditional literal.",
|
|
10
|
-
run: (context) => {
|
|
11
|
-
if (context.containerOverrides === void 0) {
|
|
12
|
-
return [];
|
|
13
|
-
}
|
|
14
|
-
return context.containerOverrides.filter((override) => override.kind === "enable_internet").map(
|
|
15
|
-
(override) => emit(containerStartEnableInternetOverride, {
|
|
16
|
-
cacheKey: `container_start_enable_internet_override:${override.file}:${override.line.toString()}`,
|
|
17
|
-
detail: `\`${override.exportName}\` calls \`.start({ enableInternet: true })\`, overriding the container's static egress posture.`,
|
|
18
|
-
metadata: { exportName: override.exportName, file: override.file, line: override.line }
|
|
19
|
-
})
|
|
20
|
-
);
|
|
21
|
-
},
|
|
22
|
-
source: "static",
|
|
23
|
-
title: "Runtime .start() override re-enables container internet access"
|
|
24
|
-
};
|
|
25
|
-
|
|
26
|
-
export { containerStartEnableInternetOverride as default };
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
const dedupeCacheKeys = (findings) => {
|
|
2
|
-
const seen = /* @__PURE__ */ new Map();
|
|
3
|
-
return findings.map((finding) => {
|
|
4
|
-
const occurrence = (seen.get(finding.cacheKey) ?? 0) + 1;
|
|
5
|
-
seen.set(finding.cacheKey, occurrence);
|
|
6
|
-
if (occurrence === 1) {
|
|
7
|
-
return finding;
|
|
8
|
-
}
|
|
9
|
-
return { ...finding, cacheKey: `${finding.cacheKey}:${occurrence.toString()}` };
|
|
10
|
-
});
|
|
11
|
-
};
|
|
12
|
-
|
|
13
|
-
export { dedupeCacheKeys };
|
|
@@ -1,57 +0,0 @@
|
|
|
1
|
-
import { e as emit } from './finding-Dm_zvzS1.mjs';
|
|
2
|
-
|
|
3
|
-
const isLeadingPrefix = (a, b) => {
|
|
4
|
-
if (a.length > b.length) {
|
|
5
|
-
return false;
|
|
6
|
-
}
|
|
7
|
-
return a.every((field, position) => field === b[position]);
|
|
8
|
-
};
|
|
9
|
-
const duplicateIndex = {
|
|
10
|
-
categories: ["PERFORMANCE"],
|
|
11
|
-
description: "A secondary index is redundant because another index already covers every lookup it serves (its columns are a leading prefix of the other's). The redundant index costs storage and is maintained on every write for no read benefit.",
|
|
12
|
-
facing: "INTERNAL",
|
|
13
|
-
level: "INFO",
|
|
14
|
-
name: "duplicate_index",
|
|
15
|
-
remediation: "Drop the redundant index; the covering index already serves its lookups.",
|
|
16
|
-
run: (context) => {
|
|
17
|
-
const findings = [];
|
|
18
|
-
for (const table of context.schema.tables) {
|
|
19
|
-
const secondary = table.indexes.filter((index) => index.kind === "index");
|
|
20
|
-
for (const candidate of secondary) {
|
|
21
|
-
if (candidate.unique === true) {
|
|
22
|
-
continue;
|
|
23
|
-
}
|
|
24
|
-
const cover = secondary.find((other) => {
|
|
25
|
-
if (other === candidate) {
|
|
26
|
-
return false;
|
|
27
|
-
}
|
|
28
|
-
if (!isLeadingPrefix(candidate.fields, other.fields)) {
|
|
29
|
-
return false;
|
|
30
|
-
}
|
|
31
|
-
const sameLength = candidate.fields.length === other.fields.length;
|
|
32
|
-
return sameLength ? candidate.name > other.name : true;
|
|
33
|
-
});
|
|
34
|
-
if (!cover) {
|
|
35
|
-
continue;
|
|
36
|
-
}
|
|
37
|
-
findings.push(
|
|
38
|
-
emit(duplicateIndex, {
|
|
39
|
-
cacheKey: `duplicate_index:${table.name}:${candidate.name}`,
|
|
40
|
-
detail: `Index "${candidate.name}" on table "${table.name}" (${candidate.fields.join(", ")}) is redundant — index "${cover.name}" (${cover.fields.join(", ")}) already covers its lookups.`,
|
|
41
|
-
metadata: {
|
|
42
|
-
coveredBy: { fields: cover.fields, name: cover.name },
|
|
43
|
-
fields: candidate.fields,
|
|
44
|
-
index: candidate.name,
|
|
45
|
-
table: table.name
|
|
46
|
-
}
|
|
47
|
-
})
|
|
48
|
-
);
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
return findings;
|
|
52
|
-
},
|
|
53
|
-
source: "static",
|
|
54
|
-
title: "Duplicate / redundant index"
|
|
55
|
-
};
|
|
56
|
-
|
|
57
|
-
export { duplicateIndex as default };
|
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
import { e as emit } from './finding-Dm_zvzS1.mjs';
|
|
2
|
-
|
|
3
|
-
const emptyIndex = {
|
|
4
|
-
categories: ["SCHEMA"],
|
|
5
|
-
description: "An index declares no columns, so it indexes nothing and can never narrow a read.",
|
|
6
|
-
facing: "INTERNAL",
|
|
7
|
-
level: "WARN",
|
|
8
|
-
name: "empty_index",
|
|
9
|
-
remediation: "Give the index its columns, or remove it.",
|
|
10
|
-
run: (context) => {
|
|
11
|
-
const findings = [];
|
|
12
|
-
for (const table of context.schema.tables) {
|
|
13
|
-
for (const index of table.indexes) {
|
|
14
|
-
if (index.kind !== "index" || index.fields.length > 0) {
|
|
15
|
-
continue;
|
|
16
|
-
}
|
|
17
|
-
findings.push(
|
|
18
|
-
emit(emptyIndex, {
|
|
19
|
-
cacheKey: `empty_index:${table.name}:${index.name}`,
|
|
20
|
-
detail: `Index "${index.name}" on table "${table.name}" declares no columns.`,
|
|
21
|
-
metadata: { index: index.name, table: table.name }
|
|
22
|
-
})
|
|
23
|
-
);
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
return findings;
|
|
27
|
-
},
|
|
28
|
-
source: "static",
|
|
29
|
-
title: "Empty index"
|
|
30
|
-
};
|
|
31
|
-
|
|
32
|
-
export { emptyIndex as default };
|
|
@@ -1,46 +0,0 @@
|
|
|
1
|
-
import { e as emit } from './finding-Dm_zvzS1.mjs';
|
|
2
|
-
|
|
3
|
-
const REQUIRED_KEYS = {
|
|
4
|
-
defineExportSink: ["name", "deliver"],
|
|
5
|
-
r2Sink: ["bucket", "name"],
|
|
6
|
-
webhookExportSink: ["name", "url"]
|
|
7
|
-
};
|
|
8
|
-
const exportSinkMisconfigured = {
|
|
9
|
-
categories: ["SCHEMA"],
|
|
10
|
-
description: "A CDC export sink (`defineExportSink` / `webhookExportSink` / `r2Sink`) is missing a required config field (a webhook with no `url`, an R2 sink with no `bucket`, or a sink with no `name`/`deliver`). The sink can never deliver a change batch, so the export tap silently drains nothing.",
|
|
11
|
-
facing: "INTERNAL",
|
|
12
|
-
level: "ERROR",
|
|
13
|
-
name: "export_sink_misconfigured",
|
|
14
|
-
remediation: "Supply the missing field: a non-empty `url` for `webhookExportSink`, a `bucket` R2 binding for `r2Sink`, or `name` + `deliver` for `defineExportSink`.",
|
|
15
|
-
run: (context) => {
|
|
16
|
-
if (context.exportSinks === void 0) {
|
|
17
|
-
return [];
|
|
18
|
-
}
|
|
19
|
-
const findings = [];
|
|
20
|
-
for (const sink of context.exportSinks) {
|
|
21
|
-
if (!sink.analyzable) {
|
|
22
|
-
continue;
|
|
23
|
-
}
|
|
24
|
-
const present = new Set(sink.presentKeys);
|
|
25
|
-
const empty = new Set(sink.emptyKeys);
|
|
26
|
-
for (const key of REQUIRED_KEYS[sink.factory] ?? []) {
|
|
27
|
-
if (present.has(key) && !empty.has(key)) {
|
|
28
|
-
continue;
|
|
29
|
-
}
|
|
30
|
-
const reason = empty.has(key) ? "is set to an empty string" : "is missing";
|
|
31
|
-
findings.push(
|
|
32
|
-
emit(exportSinkMisconfigured, {
|
|
33
|
-
cacheKey: `export_sink_misconfigured:${sink.file}:${sink.line.toString()}:${key}`,
|
|
34
|
-
detail: `\`${sink.factory}({ … })\` in ${sink.file}:${sink.line.toString()} ${reason} its required \`${key}\` field — the sink can never deliver a change batch, so the export tap drains nothing.`,
|
|
35
|
-
metadata: { factory: sink.factory, field: key, file: sink.file, line: sink.line }
|
|
36
|
-
})
|
|
37
|
-
);
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
return findings;
|
|
41
|
-
},
|
|
42
|
-
source: "static",
|
|
43
|
-
title: "Export sink is misconfigured"
|
|
44
|
-
};
|
|
45
|
-
|
|
46
|
-
export { exportSinkMisconfigured as default };
|
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
import { e as emit } from './finding-Dm_zvzS1.mjs';
|
|
2
|
-
|
|
3
|
-
const externalSourceIncrementalNoDeletePath = {
|
|
4
|
-
categories: ["SCHEMA"],
|
|
5
|
-
description: "A `.source({ mode: 'incremental' })` table declares no delete-visibility path (`reconcileEveryMs` or `softDeleteColumn`), so upstream deletes are never applied and the materialized table accumulates phantom rows.",
|
|
6
|
-
facing: "EXTERNAL",
|
|
7
|
-
level: "ERROR",
|
|
8
|
-
name: "external_source_incremental_no_delete_path",
|
|
9
|
-
remediation: "Add `reconcileEveryMs` (a periodic full-pull sweep that GCs vanished rows) or `softDeleteColumn` (an upstream tombstone column the incremental pull returns). Incremental ingest can't see a delete otherwise.",
|
|
10
|
-
run: (context) => {
|
|
11
|
-
const findings = [];
|
|
12
|
-
for (const table of context.schema.tables) {
|
|
13
|
-
const source = table.externalSource;
|
|
14
|
-
if (source === void 0) {
|
|
15
|
-
continue;
|
|
16
|
-
}
|
|
17
|
-
if (source.unanalyzable) {
|
|
18
|
-
findings.push(
|
|
19
|
-
emit(externalSourceIncrementalNoDeletePath, {
|
|
20
|
-
cacheKey: `external_source_incremental_no_delete_path:${table.name}`,
|
|
21
|
-
detail: `Table \`${table.name}\`'s \`.source(...)\` config is not a static object literal, so its \`mode\`/delete-visibility can't be verified. If it is \`mode: "incremental"\`, confirm it declares \`reconcileEveryMs\` or \`softDeleteColumn\` — an incremental source without one accumulates phantom rows on every upstream delete.`,
|
|
22
|
-
level: "WARN",
|
|
23
|
-
metadata: { table: table.name }
|
|
24
|
-
})
|
|
25
|
-
);
|
|
26
|
-
continue;
|
|
27
|
-
}
|
|
28
|
-
if (source.mode !== "incremental" || source.hasReconcile || source.hasSoftDelete) {
|
|
29
|
-
continue;
|
|
30
|
-
}
|
|
31
|
-
findings.push(
|
|
32
|
-
emit(externalSourceIncrementalNoDeletePath, {
|
|
33
|
-
cacheKey: `external_source_incremental_no_delete_path:${table.name}`,
|
|
34
|
-
detail: `Table \`${table.name}\` is \`.source({ mode: "incremental" })\` but declares neither \`reconcileEveryMs\` nor \`softDeleteColumn\` — an incremental pull never sees upstream deletes, so the materialized table would accumulate phantom rows. Add one of the two delete-visibility paths.`,
|
|
35
|
-
metadata: { table: table.name }
|
|
36
|
-
})
|
|
37
|
-
);
|
|
38
|
-
}
|
|
39
|
-
return findings;
|
|
40
|
-
},
|
|
41
|
-
source: "static",
|
|
42
|
-
title: "Incremental sourced table has no delete-visibility path"
|
|
43
|
-
};
|
|
44
|
-
|
|
45
|
-
export { externalSourceIncrementalNoDeletePath as default };
|
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
import { e as emit } from './finding-Dm_zvzS1.mjs';
|
|
2
|
-
|
|
3
|
-
const externalSourceOnGlobal = {
|
|
4
|
-
categories: ["SCHEMA"],
|
|
5
|
-
description: "A table is both `.source(...)` and `.global()`. A global table lives in the external/global tier; a sourced table is materialized into a shard DO's SQLite — the two are mutually exclusive.",
|
|
6
|
-
facing: "INTERNAL",
|
|
7
|
-
level: "ERROR",
|
|
8
|
-
name: "external_source_on_global",
|
|
9
|
-
remediation: 'Drop either `.source(...)` or `.global()`. To ingest an external database into per-tenant DOs, use `.source(...)` with `.shardBy(...)`; to read it in place as a global table, use `.global({ backend: "hyperdrive" })`.',
|
|
10
|
-
run: (context) => {
|
|
11
|
-
const findings = [];
|
|
12
|
-
for (const table of context.schema.tables) {
|
|
13
|
-
if (table.externalSource === void 0 || table.shardKind !== "global") {
|
|
14
|
-
continue;
|
|
15
|
-
}
|
|
16
|
-
findings.push(
|
|
17
|
-
emit(externalSourceOnGlobal, {
|
|
18
|
-
cacheKey: `external_source_on_global:${table.name}`,
|
|
19
|
-
detail: `Table \`${table.name}\` is both \`.source(...)\` and \`.global()\` — contradictory. A sourced table materializes into a shard DO's SQLite; a global table lives in the external tier.`,
|
|
20
|
-
metadata: { table: table.name }
|
|
21
|
-
})
|
|
22
|
-
);
|
|
23
|
-
}
|
|
24
|
-
return findings;
|
|
25
|
-
},
|
|
26
|
-
source: "static",
|
|
27
|
-
title: "Sourced table cannot also be global"
|
|
28
|
-
};
|
|
29
|
-
|
|
30
|
-
export { externalSourceOnGlobal as default };
|
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
import { e as emit } from './finding-Dm_zvzS1.mjs';
|
|
2
|
-
|
|
3
|
-
const externalSourceUnscoped = {
|
|
4
|
-
categories: ["SECURITY"],
|
|
5
|
-
description: "A `.source(...)` table that is also `.shardBy(...)` has no `tenantBy` mapper, so every tenant's Durable Object runs the same unscoped query and replicates the whole multitenant table — a cross-tenant data leak.",
|
|
6
|
-
facing: "EXTERNAL",
|
|
7
|
-
level: "ERROR",
|
|
8
|
-
name: "external_source_unscoped",
|
|
9
|
-
remediation: "Add `tenantBy: (shardKey) => [shardKey]` (matching the query's bound parameters) so each shard DO pulls only its own tenant's rows. The shard key MUST bind into the source `WHERE`.",
|
|
10
|
-
run: (context) => {
|
|
11
|
-
const findings = [];
|
|
12
|
-
for (const table of context.schema.tables) {
|
|
13
|
-
if (table.externalSource === void 0 || table.shardKind !== "shardBy") {
|
|
14
|
-
continue;
|
|
15
|
-
}
|
|
16
|
-
if (table.externalSource.unanalyzable) {
|
|
17
|
-
findings.push(
|
|
18
|
-
emit(externalSourceUnscoped, {
|
|
19
|
-
cacheKey: `external_source_unscoped:${table.name}`,
|
|
20
|
-
detail: `Table \`${table.name}\` is \`.source(...)\` + \`.shardBy(...)\` but its source config is not a static object literal, so \`tenantBy\` can't be verified. Confirm \`tenantBy\` binds the shard key into the query, or inline the config so the linter can check it — an unscoped query leaks every tenant's rows into each DO.`,
|
|
21
|
-
level: "WARN",
|
|
22
|
-
metadata: { table: table.name }
|
|
23
|
-
})
|
|
24
|
-
);
|
|
25
|
-
continue;
|
|
26
|
-
}
|
|
27
|
-
if (table.externalSource.hasTenantBy) {
|
|
28
|
-
continue;
|
|
29
|
-
}
|
|
30
|
-
findings.push(
|
|
31
|
-
emit(externalSourceUnscoped, {
|
|
32
|
-
cacheKey: `external_source_unscoped:${table.name}`,
|
|
33
|
-
detail: `Table \`${table.name}\` is \`.source(...)\` + \`.shardBy(...)\` but declares no \`tenantBy\` — every tenant DO would pull the whole table. Add a \`tenantBy\` that binds the shard key into the query.`,
|
|
34
|
-
metadata: { table: table.name }
|
|
35
|
-
})
|
|
36
|
-
);
|
|
37
|
-
}
|
|
38
|
-
return findings;
|
|
39
|
-
},
|
|
40
|
-
source: "static",
|
|
41
|
-
title: "Sourced + sharded table is not tenant-scoped"
|
|
42
|
-
};
|
|
43
|
-
|
|
44
|
-
export { externalSourceUnscoped as default };
|
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
import { e as emit } from './finding-Dm_zvzS1.mjs';
|
|
2
|
-
|
|
3
|
-
const filterWithoutIndex = {
|
|
4
|
-
categories: ["PERFORMANCE"],
|
|
5
|
-
description: "A query calls `.filter()` without a `.withIndex()` / `.withSearchIndex()`, so it loads every row in the table and filters in memory — a full table scan that gets linearly slower as the table grows.",
|
|
6
|
-
facing: "EXTERNAL",
|
|
7
|
-
level: "WARN",
|
|
8
|
-
name: "filter_without_index",
|
|
9
|
-
remediation: 'Narrow the read with `.withIndex("name", (q) => q.eq(...))` first, then `.filter()` only for what the index cannot express.',
|
|
10
|
-
run: (context) => {
|
|
11
|
-
const findings = [];
|
|
12
|
-
for (const read of context.queries ?? []) {
|
|
13
|
-
if (!read.hasFilter || read.hasIndex || read.table === "") {
|
|
14
|
-
continue;
|
|
15
|
-
}
|
|
16
|
-
const location = read.line > 0 ? `${read.file}:${read.line.toString()}` : read.file;
|
|
17
|
-
findings.push(
|
|
18
|
-
emit(filterWithoutIndex, {
|
|
19
|
-
cacheKey: `filter_without_index:${read.file}:${read.line.toString()}:${read.table}`,
|
|
20
|
-
detail: `Query on "${read.table}" at ${location} calls .filter() without an index — it loads every row of "${read.table}" and filters in memory.`,
|
|
21
|
-
metadata: { file: read.file, line: read.line, table: read.table }
|
|
22
|
-
})
|
|
23
|
-
);
|
|
24
|
-
}
|
|
25
|
-
return findings;
|
|
26
|
-
},
|
|
27
|
-
source: "static",
|
|
28
|
-
title: "Filter without index"
|
|
29
|
-
};
|
|
30
|
-
|
|
31
|
-
export { filterWithoutIndex as default };
|