@lunora/advisor 1.0.0-alpha.21 → 1.0.0-alpha.211
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 +64 -7
- package/dist/index.d.mts +4107 -2575
- package/dist/index.d.ts +4107 -2575
- package/dist/index.mjs +1 -181
- package/dist/packem_shared/MAP_VERSION-MQ5W2B6n.mjs +1 -0
- package/dist/packem_shared/actionFetchSsrf-BK-kvOPO.mjs +1 -0
- package/dist/packem_shared/actionWithoutErrorHandling-4GCBT0_z.mjs +1 -0
- package/dist/packem_shared/adminRouteWithoutGuard-DPE7LuNh.mjs +1 -0
- package/dist/packem_shared/aiRawRunEscapeHatch-BW7a8kZC.mjs +1 -0
- package/dist/packem_shared/aiRunWithoutLogging-CvHHtEN9.mjs +1 -0
- package/dist/packem_shared/aiToolSideEffectPromptInjection-9eJXRlKt.mjs +1 -0
- package/dist/packem_shared/aiUnboundedGenerationPublic-C17h6sMV.mjs +1 -0
- package/dist/packem_shared/allowUnauthenticatedShardAccessEnabled-BQufF5Kb.mjs +1 -0
- package/dist/packem_shared/argument-derived-sink-DLXFC79t.mjs +1 -0
- package/dist/packem_shared/authApiCallWithoutHeaders-CReJRsGW.mjs +1 -0
- package/dist/packem_shared/authCsrfCheckDisabled-CRIVsvb9.mjs +1 -0
- package/dist/packem_shared/authEmailVerificationDisabled-BjFlnHr2.mjs +1 -0
- package/dist/packem_shared/authScimWithoutTransactions-B5ATiz5A.mjs +1 -0
- package/dist/packem_shared/authSecureCookiesDisabled-By8PVum3.mjs +1 -0
- package/dist/packem_shared/authSessionFreshageZero-DuxX3Ln9.mjs +1 -0
- package/dist/packem_shared/authTrustedOriginsWildcard-BKIh6788.mjs +1 -0
- package/dist/packem_shared/browserAllowPrivateTargets-Cj5sizhv.mjs +1 -0
- package/dist/packem_shared/browserUserUrlWithoutAllowlist-CzDEraJB.mjs +1 -0
- package/dist/packem_shared/callSiteCallers-DArHfQSU.mjs +1 -0
- package/dist/packem_shared/circularFk-DtcWFJxK.mjs +1 -0
- package/dist/packem_shared/classifySensitivity-JnjaTGYi.mjs +1 -0
- package/dist/packem_shared/commitOrderedHardDelete-BPdwOKA7.mjs +1 -0
- package/dist/packem_shared/compareToBaseline-CUG59IOy.mjs +1 -0
- package/dist/packem_shared/containerInstanceKeyFromUserInput-Bl-tAzKV.mjs +1 -0
- package/dist/packem_shared/containerOversizedInstance-Bx89uR7E.mjs +1 -0
- package/dist/packem_shared/containerPublicInternet-BFfZf_P4.mjs +1 -0
- package/dist/packem_shared/containerRuntimeEgressRelaxation-4tMrhq1k.mjs +1 -0
- package/dist/packem_shared/containerStartEnableInternetOverride-mN_kuRtk.mjs +1 -0
- package/dist/packem_shared/crossModuleTableWrite-DjiVTAwK.mjs +1 -0
- package/dist/packem_shared/dedupeCacheKeys-DtBOHffV.mjs +1 -0
- package/dist/packem_shared/dispatch-namespace-BE_LxN7Z.mjs +1 -0
- package/dist/packem_shared/dispatchCycle-CWc5Q0cd.mjs +1 -0
- package/dist/packem_shared/duplicateIndex-Cip6-Rpu.mjs +1 -0
- package/dist/packem_shared/emptyIndex-BnHDcXza.mjs +1 -0
- package/dist/packem_shared/errorWithoutCatalog-BTfvaXHR.mjs +1 -0
- package/dist/packem_shared/exportSinkMisconfigured-JfbAx9AI.mjs +1 -0
- package/dist/packem_shared/externalSourceIncrementalNoDeletePath-BCzm3HzF.mjs +1 -0
- package/dist/packem_shared/externalSourceOnGlobal-CH7xbJ49.mjs +1 -0
- package/dist/packem_shared/externalSourceUnscoped-BxU2uSXk.mjs +1 -0
- package/dist/packem_shared/fanOutBreadth-CBtmZnoh.mjs +1 -0
- package/dist/packem_shared/filterAfterIndexScan-CV8aPq7v.mjs +1 -0
- package/dist/packem_shared/filterOnPrimaryKey-BeHVg4w3.mjs +1 -0
- package/dist/packem_shared/filterWithoutIndex-BGv3jbgg.mjs +1 -0
- package/dist/packem_shared/finding-NrKO8idM.mjs +1 -0
- package/dist/packem_shared/fk-index-DXYS17cc.mjs +1 -0
- package/dist/packem_shared/flagGatesSecurityWithUnsafeDefault-B5FBDF8c.mjs +1 -0
- package/dist/packem_shared/flagReadInSubscription-DabIhYGD.mjs +1 -0
- package/dist/packem_shared/fromServerSchema-KWJd4PLE.mjs +1 -0
- package/dist/packem_shared/geoIndexFieldNotGeopoint-BMQuok2-.mjs +1 -0
- package/dist/packem_shared/geoIndexUnused-D7C9Qr4U.mjs +1 -0
- package/dist/packem_shared/globalTableNearColumnLimit-BFbBBd6A.mjs +1 -0
- package/dist/packem_shared/gradeFromScore-KSt58rj1.mjs +1 -0
- package/dist/packem_shared/hardcodedSecret-Bw_4FYrs.mjs +1 -0
- package/dist/packem_shared/helpers-DdZ_pyR3.mjs +1 -0
- package/dist/packem_shared/hotShard-BwGYZ3Tq.mjs +1 -0
- package/dist/packem_shared/httpActionMissingAuthGuard-DhMIs1EI.mjs +1 -0
- package/dist/packem_shared/httpActionResponseHeaderInjection-mcRYd3Zw.mjs +1 -0
- package/dist/packem_shared/hyperdriveOutsideAction-CPDdGP2g.mjs +1 -0
- package/dist/packem_shared/identityUndeclaredClaimTrusted-D1sEd5lr.mjs +1 -0
- package/dist/packem_shared/imagesUrlSourceFromUserInput-BJXotuyO.mjs +1 -0
- package/dist/packem_shared/indexReferencesUnknownField-DKz4-bla.mjs +1 -0
- package/dist/packem_shared/indexUtilization-CkVPZcVe.mjs +1 -0
- package/dist/packem_shared/insertManyUnsafeUserData-DWE8d_DF.mjs +1 -0
- package/dist/packem_shared/kvUnscopedUserKeyIdor-DhymglIq.mjs +1 -0
- package/dist/packem_shared/mailInboundDispatchWithoutVerify-CWwqXyPX.mjs +1 -0
- package/dist/packem_shared/mailRecipientFromRequestInput-BVXsZ4Xf.mjs +1 -0
- package/dist/packem_shared/maskUncoveredPiiColumn-axEka4nd.mjs +1 -0
- package/dist/packem_shared/maskWeakHashStrategyOnPii-DJasagzX.mjs +1 -0
- package/dist/packem_shared/maskedRelationLeakViaWith-CIFij6vE.mjs +1 -0
- package/dist/packem_shared/mutatorFullRowReplace-BzRpZ47r.mjs +1 -0
- package/dist/packem_shared/mutatorWithoutOwnerScope-BoS81ns0.mjs +1 -0
- package/dist/packem_shared/nondeterministicQueryMutation-Bbb-AGOS.mjs +1 -0
- package/dist/packem_shared/normalizeIdUsedAsAuthorization-BXN-Can6.mjs +1 -0
- package/dist/packem_shared/notifyMissingPushConfig-DeQMoNwm.mjs +1 -0
- package/dist/packem_shared/notifySendOutsideAction-BJrDkr4h.mjs +1 -0
- package/dist/packem_shared/outputProjectionMissingOnPublicRead-DWSeo7oC.mjs +1 -0
- package/dist/packem_shared/ownerFieldFromArgsNotAuth-J8Xbz5uI.mjs +1 -0
- package/dist/packem_shared/paymentCreateWithoutAuthorize-CdenWpXD.mjs +1 -0
- package/dist/packem_shared/paymentWebhookWideTolerance-DVhJB0Ip.mjs +1 -0
- package/dist/packem_shared/plaintextSecretInWranglerVariables-bNkqmNqV.mjs +1 -0
- package/dist/packem_shared/policyReferencesUnknownTable-CIKuRZ5Y.mjs +1 -0
- package/dist/packem_shared/privilegedDispatchUnvalidatedPayload-BM3NtT57.mjs +1 -0
- package/dist/packem_shared/privilegedFanoutFromPublicProcedure-DV188gHQ.mjs +1 -0
- package/dist/packem_shared/procedure-protections-DRQotu9I.mjs +1 -0
- package/dist/packem_shared/procedureWithoutStructuredEvent-CZ_B23nM.mjs +1 -0
- package/dist/packem_shared/publicArgumentUsesAny-BDEvMA8Q.mjs +1 -0
- package/dist/packem_shared/publicMutationWithoutRatelimit-Bd-rRxnz.mjs +1 -0
- package/dist/packem_shared/publicTableRlsOptoutConfusion-CbNk75Xo.mjs +1 -0
- package/dist/packem_shared/queueWithoutDlq-BvHz3Opg.mjs +1 -0
- package/dist/packem_shared/r2sqlOutsideAction-BdLiLOAX.mjs +1 -0
- package/dist/packem_shared/ratelimitDefaultMemoryStore-Dj0Kk7t9.mjs +1 -0
- package/dist/packem_shared/ratelimitKeySpoofableOrGlobal-DHMl74hR.mjs +1 -0
- package/dist/packem_shared/ratelimitMiddlewareFailOpen-Di2t8pDc.mjs +1 -0
- package/dist/packem_shared/relationReferencesUnknownField-Cl6mik8w.mjs +1 -0
- package/dist/packem_shared/relationReferencesUnknownTable-vYjirSxf.mjs +1 -0
- package/dist/packem_shared/rlsUncoveredTable-CZ4ie4gX.mjs +1 -0
- package/dist/packem_shared/selfRescheduleWithoutProgress-Be9d1bQ0.mjs +1 -0
- package/dist/packem_shared/shapeTargetsGlobalTable-Bu3eEDic.mjs +1 -0
- package/dist/packem_shared/shapeUnknownTable-CREfNnWi.mjs +1 -0
- package/dist/packem_shared/signupMutationWithoutDisposableGating-BdKwtxDP.mjs +1 -0
- package/dist/packem_shared/softDeleteIncludeDeletedFromArgs-C7Ugg4zb.mjs +1 -0
- package/dist/packem_shared/sqlInjectionRisk-BYRllgPV.mjs +1 -0
- package/dist/packem_shared/storageGenerateUploadUrlNoContentTypePin-CrwFuo83.mjs +1 -0
- package/dist/packem_shared/storageKeyFromUserArgs-3vkxmI3v.mjs +1 -0
- package/dist/packem_shared/storagePresignedUrlForPrivateContent-C3KeV5tV.mjs +1 -0
- package/dist/packem_shared/storageUploadWithoutContentTypeAllowlist-BXoK_QWE.mjs +1 -0
- package/dist/packem_shared/storageUploadWithoutMaxSize-CW7uKvSi.mjs +1 -0
- package/dist/packem_shared/sweeps-JWMwcakB.mjs +1 -0
- package/dist/packem_shared/tableWithoutInsert-ClJp_jDr.mjs +1 -0
- package/dist/packem_shared/throughRelationMissingUniqueIndex-Cg5e3GkS.mjs +1 -0
- package/dist/packem_shared/topicTooManySubscriptions-CiZhFm9r.mjs +1 -0
- package/dist/packem_shared/ttlFieldNotTimestamp-lnb5nmM6.mjs +1 -0
- package/dist/packem_shared/unboundedCollectOnOpenRange-DHy2RUL7.mjs +1 -0
- package/dist/packem_shared/unboundedLoop-DxN3_apu.mjs +1 -0
- package/dist/packem_shared/unboundedStringArgument-jZnCbwB0.mjs +1 -0
- package/dist/packem_shared/unindexedForeignKey-Dypgn8uH.mjs +1 -0
- package/dist/packem_shared/unindexedRelationTarget-Dj2_NOCm.mjs +1 -0
- package/dist/packem_shared/unrestrictedWhereBranch-CcIBmHik.mjs +1 -0
- package/dist/packem_shared/userCreatingMutationWithoutCaptcha-D4TZEpgz.mjs +1 -0
- package/dist/packem_shared/vectorsNamespaceFromUserInput-BWvGjJzp.mjs +1 -0
- package/dist/packem_shared/workflowDuplicateStepName-B6eAJzwA.mjs +1 -0
- package/dist/packem_shared/workflowUnknownTarget-DEKB5H0g.mjs +1 -0
- package/dist/packem_shared/workflowUnused-DHwwE-1P.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-B2freHrP.mjs +0 -84
- package/dist/packem_shared/constraintValidator-Dr9Py3FD.mjs +0 -186
- 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/duplicateIndex-BOublMSt.mjs +0 -57
- package/dist/packem_shared/emptyIndex-BX8EuEY7.mjs +0 -32
- 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-BiB6JSiW.mjs +0 -59
- package/dist/packem_shared/hardcodedSecret-W2pz1UZB.mjs +0 -35
- package/dist/packem_shared/helpers-BySnKhVB.mjs +0 -31
- package/dist/packem_shared/hotShard-Ir5D0B6J.mjs +0 -48
- 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/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/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/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,186 +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 checkFkRelation = (lint, sample, relation, targetSample) => {
|
|
13
|
-
if (relation.kind !== "one") {
|
|
14
|
-
return void 0;
|
|
15
|
-
}
|
|
16
|
-
const fkColumn = relation.field;
|
|
17
|
-
const danglingIds = [];
|
|
18
|
-
for (const row of sample.rows) {
|
|
19
|
-
const fkValue = row[fkColumn];
|
|
20
|
-
if (fkValue === null || fkValue === void 0) {
|
|
21
|
-
continue;
|
|
22
|
-
}
|
|
23
|
-
const fkString = String(fkValue);
|
|
24
|
-
if (!targetSample.existingIds.has(fkString)) {
|
|
25
|
-
danglingIds.push(rowId(row));
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
if (danglingIds.length === 0) {
|
|
29
|
-
return void 0;
|
|
30
|
-
}
|
|
31
|
-
const isTruncated = sample.truncated || targetSample.truncated;
|
|
32
|
-
const cap = Math.min(sample.cap, targetSample.cap);
|
|
33
|
-
const detail = `Table "${sample.table}": ${danglingIds.length.toString()} sampled row(s) have a dangling FK value in column "${fkColumn}" — the referenced "${relation.table}" row does not exist. Row ids: ${formatExamples(danglingIds, danglingIds.length)}${truncatedSuffix(isTruncated, cap, "more rows may exist beyond the window")}.`;
|
|
34
|
-
return emit(lint, {
|
|
35
|
-
cacheKey: `constraint_validator:fk:${sample.table}:${fkColumn}`,
|
|
36
|
-
detail,
|
|
37
|
-
metadata: {
|
|
38
|
-
cap,
|
|
39
|
-
column: fkColumn,
|
|
40
|
-
count: danglingIds.length,
|
|
41
|
-
examples: danglingIds.slice(0, MAX_EXAMPLE_IDS),
|
|
42
|
-
kind: "fk",
|
|
43
|
-
referencesTable: relation.table,
|
|
44
|
-
table: sample.table,
|
|
45
|
-
truncated: isTruncated
|
|
46
|
-
}
|
|
47
|
-
});
|
|
48
|
-
};
|
|
49
|
-
const checkNullField = (lint, sample, field, isRequired) => {
|
|
50
|
-
if (!isRequired) {
|
|
51
|
-
return void 0;
|
|
52
|
-
}
|
|
53
|
-
const nullIds = [];
|
|
54
|
-
for (const row of sample.rows) {
|
|
55
|
-
const value = row[field];
|
|
56
|
-
if (value === null || value === void 0) {
|
|
57
|
-
nullIds.push(rowId(row));
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
if (nullIds.length === 0) {
|
|
61
|
-
return void 0;
|
|
62
|
-
}
|
|
63
|
-
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")}.`;
|
|
64
|
-
return emit(lint, {
|
|
65
|
-
cacheKey: `constraint_validator:null:${sample.table}:${field}`,
|
|
66
|
-
detail,
|
|
67
|
-
metadata: {
|
|
68
|
-
cap: sample.cap,
|
|
69
|
-
column: field,
|
|
70
|
-
count: nullIds.length,
|
|
71
|
-
examples: nullIds.slice(0, MAX_EXAMPLE_IDS),
|
|
72
|
-
kind: "null",
|
|
73
|
-
table: sample.table,
|
|
74
|
-
truncated: sample.truncated
|
|
75
|
-
}
|
|
76
|
-
});
|
|
77
|
-
};
|
|
78
|
-
const compositeKey = (row, fields) => {
|
|
79
|
-
const parts = [];
|
|
80
|
-
for (const field of fields) {
|
|
81
|
-
const value = row[field];
|
|
82
|
-
if (value === null || value === void 0) {
|
|
83
|
-
return void 0;
|
|
84
|
-
}
|
|
85
|
-
parts.push(JSON.stringify(value));
|
|
86
|
-
}
|
|
87
|
-
return parts.join("|");
|
|
88
|
-
};
|
|
89
|
-
const recordDuplicate = (id, existingId, duplicateIds) => {
|
|
90
|
-
if (!duplicateIds.includes(existingId)) {
|
|
91
|
-
duplicateIds.push(existingId);
|
|
92
|
-
}
|
|
93
|
-
duplicateIds.push(id);
|
|
94
|
-
};
|
|
95
|
-
const checkUniqueIndex = (lint, sample, index) => {
|
|
96
|
-
if (index.kind !== "index" || index.unique !== true) {
|
|
97
|
-
return void 0;
|
|
98
|
-
}
|
|
99
|
-
const seen = /* @__PURE__ */ new Map();
|
|
100
|
-
const duplicateIds = [];
|
|
101
|
-
for (const row of sample.rows) {
|
|
102
|
-
const id = rowId(row);
|
|
103
|
-
const key = compositeKey(row, index.fields);
|
|
104
|
-
if (key === void 0) {
|
|
105
|
-
continue;
|
|
106
|
-
}
|
|
107
|
-
const existingId = seen.get(key);
|
|
108
|
-
if (existingId === void 0) {
|
|
109
|
-
seen.set(key, id);
|
|
110
|
-
} else {
|
|
111
|
-
recordDuplicate(id, existingId, duplicateIds);
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
if (duplicateIds.length === 0) {
|
|
115
|
-
return void 0;
|
|
116
|
-
}
|
|
117
|
-
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")}.`;
|
|
118
|
-
return emit(lint, {
|
|
119
|
-
cacheKey: `constraint_validator:unique:${sample.table}:${index.name}`,
|
|
120
|
-
detail,
|
|
121
|
-
metadata: {
|
|
122
|
-
cap: sample.cap,
|
|
123
|
-
count: duplicateIds.length,
|
|
124
|
-
examples: duplicateIds.slice(0, MAX_EXAMPLE_IDS),
|
|
125
|
-
fields: index.fields,
|
|
126
|
-
index: index.name,
|
|
127
|
-
kind: "unique",
|
|
128
|
-
table: sample.table,
|
|
129
|
-
truncated: sample.truncated
|
|
130
|
-
}
|
|
131
|
-
});
|
|
132
|
-
};
|
|
133
|
-
const checkTable = (lint, sample, sampleByTable, tableByName) => {
|
|
134
|
-
const schemaTable = tableByName.get(sample.table);
|
|
135
|
-
if (!schemaTable) {
|
|
136
|
-
return [];
|
|
137
|
-
}
|
|
138
|
-
const findings = [];
|
|
139
|
-
for (const relation of schemaTable.relations) {
|
|
140
|
-
const targetSample = sampleByTable.get(relation.table);
|
|
141
|
-
if (!targetSample) {
|
|
142
|
-
continue;
|
|
143
|
-
}
|
|
144
|
-
const f = checkFkRelation(lint, sample, relation, targetSample);
|
|
145
|
-
if (f) {
|
|
146
|
-
findings.push(f);
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
for (const field of schemaTable.fields) {
|
|
150
|
-
const isRequired = !schemaTable.optionalFields?.has(field);
|
|
151
|
-
const f = checkNullField(lint, sample, field, isRequired);
|
|
152
|
-
if (f) {
|
|
153
|
-
findings.push(f);
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
for (const index of schemaTable.indexes) {
|
|
157
|
-
const f = checkUniqueIndex(lint, sample, index);
|
|
158
|
-
if (f) {
|
|
159
|
-
findings.push(f);
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
return findings;
|
|
163
|
-
};
|
|
164
|
-
const constraintValidator = {
|
|
165
|
-
categories: ["SCHEMA"],
|
|
166
|
-
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).",
|
|
167
|
-
facing: "INTERNAL",
|
|
168
|
-
level: "WARN",
|
|
169
|
-
name: "constraint_validator",
|
|
170
|
-
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.",
|
|
171
|
-
run: (context) => {
|
|
172
|
-
if (!context.tableSamples || context.tableSamples.length === 0) {
|
|
173
|
-
return [];
|
|
174
|
-
}
|
|
175
|
-
const sampleByTable = /* @__PURE__ */ new Map();
|
|
176
|
-
for (const sample of context.tableSamples) {
|
|
177
|
-
sampleByTable.set(sample.table, sample);
|
|
178
|
-
}
|
|
179
|
-
const tableByName = new Map(context.schema.tables.map((table) => [table.name, table]));
|
|
180
|
-
return context.tableSamples.flatMap((sample) => checkTable(constraintValidator, sample, sampleByTable, tableByName));
|
|
181
|
-
},
|
|
182
|
-
source: "runtime",
|
|
183
|
-
title: "Constraint violation"
|
|
184
|
-
};
|
|
185
|
-
|
|
186
|
-
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,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,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 };
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
const emit = (lint, occurrence) => {
|
|
2
|
-
return {
|
|
3
|
-
cacheKey: occurrence.cacheKey,
|
|
4
|
-
categories: occurrence.categories ?? lint.categories,
|
|
5
|
-
description: lint.description,
|
|
6
|
-
detail: occurrence.detail,
|
|
7
|
-
facing: occurrence.facing ?? lint.facing,
|
|
8
|
-
level: occurrence.level ?? lint.level,
|
|
9
|
-
metadata: occurrence.metadata,
|
|
10
|
-
name: lint.name,
|
|
11
|
-
remediation: lint.remediation,
|
|
12
|
-
title: lint.title
|
|
13
|
-
};
|
|
14
|
-
};
|
|
15
|
-
|
|
16
|
-
export { emit as e };
|
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
const PRIMARY_KEY = "_id";
|
|
2
|
-
const suggestIndexName = (field) => `by${field.charAt(0).toUpperCase()}${field.slice(1)}`;
|
|
3
|
-
const leadingIndexedColumns = (table) => new Set(
|
|
4
|
-
table.indexes.filter((index) => index.kind === "index").map((index) => index.fields[0]).filter((field) => field !== void 0)
|
|
5
|
-
);
|
|
6
|
-
|
|
7
|
-
export { PRIMARY_KEY as P, leadingIndexedColumns as l, suggestIndexName as s };
|
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
import { e as emit } from './finding-Dm_zvzS1.mjs';
|
|
2
|
-
|
|
3
|
-
const PROTECT_TOKENS = /* @__PURE__ */ new Set(["enforce", "gate", "lockdown", "rls"]);
|
|
4
|
-
const PERMIT_TOKENS = /* @__PURE__ */ new Set(["allow", "bypass", "permit"]);
|
|
5
|
-
const ACRONYM_WORD_BOUNDARY = /(?<=[A-Z])(?=[A-Z][a-z])/gu;
|
|
6
|
-
const CAMEL_HUMP_BOUNDARY = /(?<=[a-z0-9])(?=[A-Z])/gu;
|
|
7
|
-
const NON_ALPHANUMERIC_RUN = /[^a-z0-9]+/u;
|
|
8
|
-
const tokenize = (key) => key.replaceAll(ACRONYM_WORD_BOUNDARY, " ").replaceAll(CAMEL_HUMP_BOUNDARY, " ").toLowerCase().split(NON_ALPHANUMERIC_RUN).filter((token) => token.length > 0);
|
|
9
|
-
const hasUnsafeDefault = (row) => {
|
|
10
|
-
const tokens = tokenize(row.key);
|
|
11
|
-
const isProtect = tokens.some((token) => PROTECT_TOKENS.has(token));
|
|
12
|
-
const isPermit = tokens.some((token) => PERMIT_TOKENS.has(token));
|
|
13
|
-
if (isProtect === isPermit) {
|
|
14
|
-
return false;
|
|
15
|
-
}
|
|
16
|
-
return isProtect ? !row.defaultValue : row.defaultValue;
|
|
17
|
-
};
|
|
18
|
-
const flagGatesSecurityWithUnsafeDefault = {
|
|
19
|
-
categories: ["SECURITY"],
|
|
20
|
-
description: "A `ctx.flags.boolean(key, default)` read on a security-shaped key has a fail-open default that selects the permissive branch. OpenFeature returns the default when the provider errors, so an outage silently disables a protection (a `false` default on an `enforce`/`rls`/`gate`/`lockdown` key) or grants a permission (a `true` default on an `allow`/`permit`/`bypass` key).",
|
|
21
|
-
facing: "EXTERNAL",
|
|
22
|
-
level: "WARN",
|
|
23
|
-
name: "flag_gates_security_with_unsafe_default",
|
|
24
|
-
remediation: "Flip the default so a provider outage fails closed: default a protection flag (`enforce*`/`rls*`/`gate*`/`lockdown*`) to `true`, and a permission/bypass flag (`allow*`/`permit*`/`bypass*`) to `false`. The safe default is always the restrictive branch — never let an unreachable flag backend open access.",
|
|
25
|
-
run: (context) => {
|
|
26
|
-
if (context.flagSecurityDefaults === void 0) {
|
|
27
|
-
return [];
|
|
28
|
-
}
|
|
29
|
-
return context.flagSecurityDefaults.filter((row) => hasUnsafeDefault(row)).map(
|
|
30
|
-
(row) => emit(flagGatesSecurityWithUnsafeDefault, {
|
|
31
|
-
cacheKey: `flag_gates_security_with_unsafe_default:${row.file}:${row.line.toString()}`,
|
|
32
|
-
detail: `\`ctx.flags.boolean("${row.key}", ${String(row.defaultValue)})\` in \`${row.exportName}\` (${row.file}:${row.line.toString()}) fails open to the permissive branch — a provider outage returns \`${String(row.defaultValue)}\`, ${row.defaultValue ? "granting the guarded permission" : "disabling the guarded protection"}.`,
|
|
33
|
-
metadata: { defaultValue: row.defaultValue, exportName: row.exportName, file: row.file, key: row.key, line: row.line }
|
|
34
|
-
})
|
|
35
|
-
);
|
|
36
|
-
},
|
|
37
|
-
source: "static",
|
|
38
|
-
title: "Security flag fails open to the permissive branch"
|
|
39
|
-
};
|
|
40
|
-
|
|
41
|
-
export { flagGatesSecurityWithUnsafeDefault as default };
|
|
@@ -1,59 +0,0 @@
|
|
|
1
|
-
const fromServerSchema = (schema) => {
|
|
2
|
-
return {
|
|
3
|
-
rlsMode: schema.rlsMode,
|
|
4
|
-
tables: Object.entries(schema.tables).map(([name, table]) => {
|
|
5
|
-
const indexes = [
|
|
6
|
-
...table.indexes.map((index) => {
|
|
7
|
-
return { fields: index.fields, kind: "index", name: index.name, unique: index.unique };
|
|
8
|
-
}),
|
|
9
|
-
...table.searchIndexes.map((index) => {
|
|
10
|
-
return { fields: [index.field, ...index.filterFields ?? []], kind: "search", name: index.name };
|
|
11
|
-
}),
|
|
12
|
-
...table.rankIndexes.map((index) => {
|
|
13
|
-
return { fields: [...index.sortBy.map((key) => key.field), ...index.partitionBy ?? []], kind: "rank", name: index.name };
|
|
14
|
-
}),
|
|
15
|
-
...table.vectorIndexes.map((index) => {
|
|
16
|
-
return { fields: [index.field], kind: "vector", name: index.name };
|
|
17
|
-
})
|
|
18
|
-
];
|
|
19
|
-
const optionalFields = /* @__PURE__ */ new Set();
|
|
20
|
-
for (const [fieldName, validator] of Object.entries(table.shape)) {
|
|
21
|
-
if (validator.kind === "optional") {
|
|
22
|
-
optionalFields.add(fieldName);
|
|
23
|
-
} else {
|
|
24
|
-
const column = validator._meta?.column;
|
|
25
|
-
if (column?.notNull === false) {
|
|
26
|
-
optionalFields.add(fieldName);
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
return {
|
|
31
|
-
externallyManaged: table.isExternallyManaged ?? false,
|
|
32
|
-
externalSource: table.externalSource ? {
|
|
33
|
-
hasReconcile: table.externalSource.reconcileEveryMs !== void 0,
|
|
34
|
-
hasTenantBy: table.externalSource.tenantBy !== void 0,
|
|
35
|
-
mode: table.externalSource.mode
|
|
36
|
-
} : void 0,
|
|
37
|
-
fields: Object.keys(table.shape),
|
|
38
|
-
indexes,
|
|
39
|
-
isPublic: table.isPublic ?? false,
|
|
40
|
-
name,
|
|
41
|
-
optionalFields,
|
|
42
|
-
shardKind: table.shardMode.kind,
|
|
43
|
-
softDelete: table.softDeleteMode,
|
|
44
|
-
relations: Object.entries(table.relationMap).map(([accessor, relation]) => {
|
|
45
|
-
return {
|
|
46
|
-
field: relation.field,
|
|
47
|
-
kind: relation.kind,
|
|
48
|
-
name: accessor,
|
|
49
|
-
onDelete: relation.onDelete,
|
|
50
|
-
references: relation.references,
|
|
51
|
-
table: relation.table
|
|
52
|
-
};
|
|
53
|
-
})
|
|
54
|
-
};
|
|
55
|
-
})
|
|
56
|
-
};
|
|
57
|
-
};
|
|
58
|
-
|
|
59
|
-
export { fromServerSchema };
|