@lunora/advisor 1.0.0-alpha.34 → 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 +2 -2
- 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,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,70 +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
|
-
...table.geoIndexes.map((index) => {
|
|
19
|
-
return { fields: [index.field], kind: "geo", name: index.name };
|
|
20
|
-
})
|
|
21
|
-
];
|
|
22
|
-
const columnKinds = {};
|
|
23
|
-
for (const [fieldName, validator] of Object.entries(table.shape)) {
|
|
24
|
-
const inner = validator.kind === "optional" ? validator._meta?.inner : validator;
|
|
25
|
-
columnKinds[fieldName] = inner?.kind ?? validator.kind;
|
|
26
|
-
}
|
|
27
|
-
const optionalFields = /* @__PURE__ */ new Set();
|
|
28
|
-
for (const [fieldName, validator] of Object.entries(table.shape)) {
|
|
29
|
-
if (validator.kind === "optional") {
|
|
30
|
-
optionalFields.add(fieldName);
|
|
31
|
-
} else {
|
|
32
|
-
const column = validator._meta?.column;
|
|
33
|
-
if (column?.notNull === false) {
|
|
34
|
-
optionalFields.add(fieldName);
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
return {
|
|
39
|
-
externallyManaged: table.isExternallyManaged ?? false,
|
|
40
|
-
externalSource: table.externalSource ? {
|
|
41
|
-
hasReconcile: table.externalSource.reconcileEveryMs !== void 0,
|
|
42
|
-
hasSoftDelete: table.externalSource.softDeleteColumn !== void 0,
|
|
43
|
-
hasTenantBy: table.externalSource.tenantBy !== void 0,
|
|
44
|
-
mode: table.externalSource.mode
|
|
45
|
-
} : void 0,
|
|
46
|
-
columnKinds,
|
|
47
|
-
fields: Object.keys(table.shape),
|
|
48
|
-
indexes,
|
|
49
|
-
isPublic: table.isPublic ?? false,
|
|
50
|
-
name,
|
|
51
|
-
optionalFields,
|
|
52
|
-
shardKind: table.shardMode.kind,
|
|
53
|
-
softDelete: table.softDeleteMode,
|
|
54
|
-
ttl: table.ttlPolicy,
|
|
55
|
-
relations: Object.entries(table.relationMap).map(([accessor, relation]) => {
|
|
56
|
-
return {
|
|
57
|
-
field: relation.field,
|
|
58
|
-
kind: relation.kind,
|
|
59
|
-
name: accessor,
|
|
60
|
-
onDelete: relation.onDelete,
|
|
61
|
-
references: relation.references,
|
|
62
|
-
table: relation.table
|
|
63
|
-
};
|
|
64
|
-
})
|
|
65
|
-
};
|
|
66
|
-
})
|
|
67
|
-
};
|
|
68
|
-
};
|
|
69
|
-
|
|
70
|
-
export { fromServerSchema };
|
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
import { e as emit } from './finding-Dm_zvzS1.mjs';
|
|
2
|
-
|
|
3
|
-
const geoIndexFieldNotGeopoint = {
|
|
4
|
-
categories: ["SCHEMA"],
|
|
5
|
-
description: "A `.geoIndex(name, { field })` points at a column that is not a `v.geoPoint()`, so its geohash companion can't be built and `withGeoIndex(...)` can never match.",
|
|
6
|
-
facing: "INTERNAL",
|
|
7
|
-
level: "ERROR",
|
|
8
|
-
name: "geo_index_field_not_geopoint",
|
|
9
|
-
remediation: "Point the geo index at a `v.geoPoint()` column, or change the column's type to `v.geoPoint()`.",
|
|
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 !== "geo") {
|
|
15
|
-
continue;
|
|
16
|
-
}
|
|
17
|
-
const field = index.fields[0];
|
|
18
|
-
if (field === void 0) {
|
|
19
|
-
continue;
|
|
20
|
-
}
|
|
21
|
-
const kind = table.columnKinds?.[field];
|
|
22
|
-
if (kind === void 0 || kind === "geoPoint") {
|
|
23
|
-
continue;
|
|
24
|
-
}
|
|
25
|
-
findings.push(
|
|
26
|
-
emit(geoIndexFieldNotGeopoint, {
|
|
27
|
-
cacheKey: `geo_index_field_not_geopoint:${table.name}:${index.name}:${field}`,
|
|
28
|
-
detail: `Geo index "${index.name}" on table "${table.name}" indexes column "${field}", which is a ${kind}, not a v.geoPoint().`,
|
|
29
|
-
metadata: { field, index: index.name, kind, table: table.name }
|
|
30
|
-
})
|
|
31
|
-
);
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
return findings;
|
|
35
|
-
},
|
|
36
|
-
source: "static",
|
|
37
|
-
title: "Geo index field is not a geoPoint"
|
|
38
|
-
};
|
|
39
|
-
|
|
40
|
-
export { geoIndexFieldNotGeopoint as default };
|
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
import { e as emit } from './finding-Dm_zvzS1.mjs';
|
|
2
|
-
|
|
3
|
-
const geoIndexUnused = {
|
|
4
|
-
categories: ["SCHEMA"],
|
|
5
|
-
description: "A `.geoIndex(name, { field })` is declared but no handler reads it via `withGeoIndex(name, …)`. The geohash companion column is maintained on every write and read by nothing — dead overhead, the geo analogue of a dead index.",
|
|
6
|
-
facing: "INTERNAL",
|
|
7
|
-
level: "INFO",
|
|
8
|
-
name: "geo_index_unused",
|
|
9
|
-
remediation: 'Query the index from a handler with `ctx.db.query("<table>").withGeoIndex("<name>", q => q.near(point, radius))`, or drop the `.geoIndex(...)` declaration so writes stop maintaining its geohash companion.',
|
|
10
|
-
run: (context) => {
|
|
11
|
-
if (context.geoIndexUsages === void 0) {
|
|
12
|
-
return [];
|
|
13
|
-
}
|
|
14
|
-
const usages = context.geoIndexUsages;
|
|
15
|
-
if (usages.some((usage) => usage.indexName === "")) {
|
|
16
|
-
return [];
|
|
17
|
-
}
|
|
18
|
-
const used = new Set(usages.map((usage) => usage.indexName));
|
|
19
|
-
const findings = [];
|
|
20
|
-
for (const table of context.schema.tables) {
|
|
21
|
-
for (const index of table.indexes) {
|
|
22
|
-
if (index.kind !== "geo" || used.has(index.name)) {
|
|
23
|
-
continue;
|
|
24
|
-
}
|
|
25
|
-
findings.push(
|
|
26
|
-
emit(geoIndexUnused, {
|
|
27
|
-
cacheKey: `geo_index_unused:${table.name}:${index.name}`,
|
|
28
|
-
detail: `No handler reads geo index "${index.name}" on table "${table.name}" via \`withGeoIndex("${index.name}", …)\` — it's declared but never queried, so its geohash companion is maintained on every write for nothing.`,
|
|
29
|
-
metadata: { index: index.name, table: table.name }
|
|
30
|
-
})
|
|
31
|
-
);
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
return findings;
|
|
35
|
-
},
|
|
36
|
-
source: "static",
|
|
37
|
-
title: "Geo index is declared but never queried"
|
|
38
|
-
};
|
|
39
|
-
|
|
40
|
-
export { geoIndexUnused as default };
|
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
import { dedupeCacheKeys } from './dedupeCacheKeys-r5B7u_yq.mjs';
|
|
2
|
-
import { e as emit } from './finding-Dm_zvzS1.mjs';
|
|
3
|
-
|
|
4
|
-
const hardcodedSecret = {
|
|
5
|
-
categories: ["SECURITY"],
|
|
6
|
-
description: "A secret-shaped string literal (live API key, access key, private key, or high-entropy token) is hard-coded in the source. Committed secrets leak via clone/fork/history and force a redeploy to rotate.",
|
|
7
|
-
facing: "INTERNAL",
|
|
8
|
-
level: "ERROR",
|
|
9
|
-
name: "hardcoded_secret",
|
|
10
|
-
remediation: 'Move the secret out of source: set it locally in `.dev.vars` (`lunora env set <NAME> "<value>"`) and in production with `wrangler secret put <NAME>`, then read it at runtime from `env.<NAME>`. Rotate the exposed value — assume it is already compromised.',
|
|
11
|
-
run: (context) => {
|
|
12
|
-
if (context.secretLiterals === void 0) {
|
|
13
|
-
return [];
|
|
14
|
-
}
|
|
15
|
-
const findings = [];
|
|
16
|
-
for (const secret of context.secretLiterals) {
|
|
17
|
-
findings.push(
|
|
18
|
-
emit(hardcodedSecret, {
|
|
19
|
-
cacheKey: `hardcoded_secret:${secret.file}:${secret.line.toString()}:${secret.kind}`,
|
|
20
|
-
detail: `A ${secret.kind.replaceAll("_", " ")} (${secret.preview}) is hard-coded at ${secret.file}:${secret.line.toString()}. Move it to \`.dev.vars\` / \`wrangler secret put\` and read it from \`env\`. Rotate the exposed value.`,
|
|
21
|
-
metadata: { file: secret.file, kind: secret.kind, line: secret.line, preview: secret.preview }
|
|
22
|
-
})
|
|
23
|
-
);
|
|
24
|
-
}
|
|
25
|
-
return dedupeCacheKeys(findings);
|
|
26
|
-
},
|
|
27
|
-
source: "static",
|
|
28
|
-
title: "Hard-coded secret in source"
|
|
29
|
-
};
|
|
30
|
-
|
|
31
|
-
export { hardcodedSecret as default };
|
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
const OWNERSHIP_FIELD_NAMES = /* @__PURE__ */ new Set([
|
|
2
|
-
"accountId",
|
|
3
|
-
"authorId",
|
|
4
|
-
"createdBy",
|
|
5
|
-
"createdById",
|
|
6
|
-
"organizationId",
|
|
7
|
-
"orgId",
|
|
8
|
-
"ownerId",
|
|
9
|
-
"tenantId",
|
|
10
|
-
"updatedBy",
|
|
11
|
-
"userId",
|
|
12
|
-
"workspaceId"
|
|
13
|
-
]);
|
|
14
|
-
const PII_FIELD_NAMES = /* @__PURE__ */ new Set([
|
|
15
|
-
"address",
|
|
16
|
-
"dateOfBirth",
|
|
17
|
-
"dob",
|
|
18
|
-
"email",
|
|
19
|
-
"firstName",
|
|
20
|
-
"lastName",
|
|
21
|
-
"phone",
|
|
22
|
-
"phoneNumber",
|
|
23
|
-
"socialSecurityNumber",
|
|
24
|
-
"ssn"
|
|
25
|
-
]);
|
|
26
|
-
const SYSTEM_FIELDS = /* @__PURE__ */ new Set(["_creationTime", "_id"]);
|
|
27
|
-
const ownershipOrPiiColumns = (table) => table.fields.filter((field) => OWNERSHIP_FIELD_NAMES.has(field) || PII_FIELD_NAMES.has(field));
|
|
28
|
-
const tableColumnSet = (table) => /* @__PURE__ */ new Set([...SYSTEM_FIELDS, ...table.fields]);
|
|
29
|
-
const isPublicWrite = (procedure) => procedure.visibility === "public" && (procedure.kind === "mutation" || procedure.kind === "action");
|
|
30
|
-
|
|
31
|
-
export { PII_FIELD_NAMES as P, isPublicWrite as i, ownershipOrPiiColumns as o, tableColumnSet as t };
|
|
@@ -1,60 +0,0 @@
|
|
|
1
|
-
import { e as emit } from './finding-Dm_zvzS1.mjs';
|
|
2
|
-
|
|
3
|
-
const HOT_SHARE_THRESHOLD = 0.5;
|
|
4
|
-
const MIN_TOTAL_REQUESTS = 50;
|
|
5
|
-
const hotShard = {
|
|
6
|
-
categories: ["PERFORMANCE"],
|
|
7
|
-
description: "One shard is taking a disproportionate share of a sharded function's traffic, so the busy shard becomes a single-DO bottleneck while its siblings sit idle — the hot-key skew sharding is meant to avoid.",
|
|
8
|
-
facing: "EXTERNAL",
|
|
9
|
-
level: "WARN",
|
|
10
|
-
name: "hot_shard",
|
|
11
|
-
remediation: "Re-shard on a higher-cardinality / more evenly-distributed key, or split the hot entity's state, so request volume spreads across shards instead of concentrating on one.",
|
|
12
|
-
run: (context) => {
|
|
13
|
-
const active = (context.shardTraffic ?? []).filter((shard) => shard.requests > 0);
|
|
14
|
-
const byGroup = /* @__PURE__ */ new Map();
|
|
15
|
-
for (const shard of active) {
|
|
16
|
-
const groupKey = shard.group ?? "";
|
|
17
|
-
const bucket = byGroup.get(groupKey);
|
|
18
|
-
if (bucket === void 0) {
|
|
19
|
-
byGroup.set(groupKey, [shard]);
|
|
20
|
-
} else {
|
|
21
|
-
bucket.push(shard);
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
const findings = [];
|
|
25
|
-
for (const [groupKey, groupShards] of byGroup) {
|
|
26
|
-
const totalRequests = groupShards.reduce((sum, shard) => sum + shard.requests, 0);
|
|
27
|
-
if (groupShards.length < 2 || totalRequests < MIN_TOTAL_REQUESTS) {
|
|
28
|
-
continue;
|
|
29
|
-
}
|
|
30
|
-
for (const shard of groupShards) {
|
|
31
|
-
const share = shard.requests / totalRequests;
|
|
32
|
-
if (share < HOT_SHARE_THRESHOLD) {
|
|
33
|
-
continue;
|
|
34
|
-
}
|
|
35
|
-
const scope = shard.group !== void 0 && shard.group !== "" ? `"${shard.group}" ` : "";
|
|
36
|
-
const label = shard.shardKey === "" ? "the root shard" : `shard "${shard.shardKey}"`;
|
|
37
|
-
const percent = Math.round(share * 100);
|
|
38
|
-
findings.push(
|
|
39
|
-
emit(hotShard, {
|
|
40
|
-
cacheKey: `hot_shard:${groupKey}:${shard.shardKey}`,
|
|
41
|
-
detail: `${scope}${label} handled ${shard.requests.toString()} of ${totalRequests.toString()} requests (${percent.toString()}%) across ${groupShards.length.toString()} shards — a hot-key skew. Re-shard on a more evenly-distributed key.`,
|
|
42
|
-
metadata: {
|
|
43
|
-
group: shard.group,
|
|
44
|
-
requests: shard.requests,
|
|
45
|
-
shardCount: groupShards.length,
|
|
46
|
-
shardKey: shard.shardKey,
|
|
47
|
-
share,
|
|
48
|
-
totalRequests
|
|
49
|
-
}
|
|
50
|
-
})
|
|
51
|
-
);
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
return findings;
|
|
55
|
-
},
|
|
56
|
-
source: "runtime",
|
|
57
|
-
title: "Hot shard"
|
|
58
|
-
};
|
|
59
|
-
|
|
60
|
-
export { hotShard as default };
|
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
import { e as emit } from './finding-Dm_zvzS1.mjs';
|
|
2
|
-
|
|
3
|
-
const surfaceLabel = (kind, method) => kind === "httpRoute" && method ? `\`httpRoute\` ${method} handler` : "`httpAction` handler";
|
|
4
|
-
const httpActionMissingAuthGuard = {
|
|
5
|
-
categories: ["SECURITY"],
|
|
6
|
-
description: "An `httpAction`/`httpRoute` handler performs a side effect (`ctx.runMutation`/`ctx.runAction`/a `ctx.db` write) but never reads `ctx.auth` — an unauthenticated HTTP endpoint driving a state change, bypassing the identity/RLS checks that guard the rest of the app.",
|
|
7
|
-
facing: "EXTERNAL",
|
|
8
|
-
level: "WARN",
|
|
9
|
-
name: "http_action_missing_auth_guard",
|
|
10
|
-
remediation: "Read `ctx.auth` in the handler before the side effect — call `await ctx.auth.getIdentity()` (or check `ctx.auth.userId`) and reject unauthenticated/unauthorized requests, or forward through a `mutation`/`action` whose RLS policies enforce access. If the endpoint is intentionally public (e.g. a signed webhook), verify the provider signature before the write.",
|
|
11
|
-
run: (context) => {
|
|
12
|
-
if (context.httpActionGuards === void 0) {
|
|
13
|
-
return [];
|
|
14
|
-
}
|
|
15
|
-
return context.httpActionGuards.filter((row) => !row.readsAuth).map((row) => {
|
|
16
|
-
const surface = surfaceLabel(row.kind, row.method);
|
|
17
|
-
return emit(httpActionMissingAuthGuard, {
|
|
18
|
-
cacheKey: `http_action_missing_auth_guard:${row.file}:${row.line.toString()}`,
|
|
19
|
-
detail: `${surface} \`${row.exportName}\` (${row.file}:${row.line.toString()}) calls \`ctx.${row.sideEffect}\` but never reads \`ctx.auth\` — an anonymous caller can drive this write. Authenticate the request before the side effect.`,
|
|
20
|
-
metadata: {
|
|
21
|
-
exportName: row.exportName,
|
|
22
|
-
file: row.file,
|
|
23
|
-
kind: row.kind,
|
|
24
|
-
line: row.line,
|
|
25
|
-
sideEffect: row.sideEffect,
|
|
26
|
-
...row.method ? { method: row.method } : {}
|
|
27
|
-
}
|
|
28
|
-
});
|
|
29
|
-
});
|
|
30
|
-
},
|
|
31
|
-
source: "static",
|
|
32
|
-
title: "Unauthenticated HTTP handler performs a side effect"
|
|
33
|
-
};
|
|
34
|
-
|
|
35
|
-
export { httpActionMissingAuthGuard as default };
|
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
import { e as emit } from './finding-Dm_zvzS1.mjs';
|
|
2
|
-
|
|
3
|
-
const VIA_LABELS = {
|
|
4
|
-
"headers-append": "a `headers.append(...)` call",
|
|
5
|
-
"headers-ctor": "a `new Headers({...})` initializer",
|
|
6
|
-
"headers-set": "a `headers.set(...)` call",
|
|
7
|
-
"response-init": "a `Response` init `headers` object"
|
|
8
|
-
};
|
|
9
|
-
const httpActionResponseHeaderInjection = {
|
|
10
|
-
categories: ["SECURITY"],
|
|
11
|
-
description: "An `httpAction` handler writes a response-header value derived from raw request input (`request.headers`/URL/query/body) with no CR/LF sanitizer. A `Request`-derived string placed verbatim into a response header lets a caller smuggle `\\r\\n` and inject extra headers or split the response (HTTP response splitting / header injection).",
|
|
12
|
-
facing: "EXTERNAL",
|
|
13
|
-
level: "WARN",
|
|
14
|
-
name: "http_action_response_header_injection",
|
|
15
|
-
remediation: "Never place a raw request value into a response header. Route it through a CR/LF guard (`isSafeHeaderValue` from `@lunora/server`, rejecting values containing `\\r`/`\\n`/`\\0`), URL-encode it (`encodeURIComponent`), or coerce it to a number — before writing the header. `String(...)` / `.toString()` do NOT strip CR/LF and are not sufficient.",
|
|
16
|
-
run: (context) => {
|
|
17
|
-
if (context.httpHeaderWrites === void 0) {
|
|
18
|
-
return [];
|
|
19
|
-
}
|
|
20
|
-
return context.httpHeaderWrites.map((row) => {
|
|
21
|
-
const header = row.headerName === "" ? "a response header" : `the \`${row.headerName}\` response header`;
|
|
22
|
-
return emit(httpActionResponseHeaderInjection, {
|
|
23
|
-
cacheKey: `http_action_response_header_injection:${row.file}:${row.line.toString()}`,
|
|
24
|
-
detail: `\`${row.exportName}\` (${row.file}:${row.line.toString()}) writes ${header} from raw request input via ${VIA_LABELS[row.via]} with no CR/LF guard — a caller can smuggle \`\\r\\n\` to inject headers or split the response. Route the value through \`isSafeHeaderValue\` or \`encodeURIComponent\` first.`,
|
|
25
|
-
metadata: {
|
|
26
|
-
exportName: row.exportName,
|
|
27
|
-
file: row.file,
|
|
28
|
-
headerName: row.headerName,
|
|
29
|
-
line: row.line,
|
|
30
|
-
via: row.via
|
|
31
|
-
}
|
|
32
|
-
});
|
|
33
|
-
});
|
|
34
|
-
},
|
|
35
|
-
source: "static",
|
|
36
|
-
title: "Response header written from unsanitized request input"
|
|
37
|
-
};
|
|
38
|
-
|
|
39
|
-
export { httpActionResponseHeaderInjection as default };
|
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
import { e as emit } from './finding-Dm_zvzS1.mjs';
|
|
2
|
-
|
|
3
|
-
const hyperdriveOutsideAction = {
|
|
4
|
-
categories: ["SCHEMA"],
|
|
5
|
-
description: "A `query`/`mutation` handler accesses Hyperdrive via `ctx.sql`. Hyperdrive hits an external database Lunora does not own: queries are non-deterministic (like `fetch`) and external writes are invisible to live queries. `ctx.sql` is available on `ActionCtx` only and must be confined to `action` handlers.",
|
|
6
|
-
facing: "EXTERNAL",
|
|
7
|
-
level: "WARN",
|
|
8
|
-
name: "hyperdrive_outside_action",
|
|
9
|
-
remediation: "Move the `ctx.sql` access into an `action(...)` (the only context where it is typed), where external I/O is allowed. If a query/mutation needs the data, have the action read it via `ctx.sql` and write a projection into a `defineSchema` DO/D1 table — that write is tracked by live queries, whereas the external DB is not.",
|
|
10
|
-
run: (context) => {
|
|
11
|
-
if (context.hyperdriveCalls === void 0) {
|
|
12
|
-
return [];
|
|
13
|
-
}
|
|
14
|
-
const findings = [];
|
|
15
|
-
for (const call of context.hyperdriveCalls) {
|
|
16
|
-
findings.push(
|
|
17
|
-
emit(hyperdriveOutsideAction, {
|
|
18
|
-
cacheKey: `hyperdrive_outside_action:${call.file}:${call.line.toString()}:${call.callee}`,
|
|
19
|
-
detail: `\`${call.callee}(…)\` in ${call.exportName} (${call.file}:${call.line.toString()}) runs inside a ${call.kind} handler — Hyperdrive's \`ctx.sql\` is non-deterministic and non-reactive, so it is available only in actions. Move the external SQL into an \`action\` and project the result into a Lunora table if a query/mutation needs it.`,
|
|
20
|
-
metadata: { callee: call.callee, exportName: call.exportName, file: call.file, kind: call.kind, line: call.line }
|
|
21
|
-
})
|
|
22
|
-
);
|
|
23
|
-
}
|
|
24
|
-
return findings;
|
|
25
|
-
},
|
|
26
|
-
source: "static",
|
|
27
|
-
title: "Hyperdrive `ctx.sql` used outside an action"
|
|
28
|
-
};
|
|
29
|
-
|
|
30
|
-
export { hyperdriveOutsideAction as default };
|
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
import { e as emit } from './finding-Dm_zvzS1.mjs';
|
|
2
|
-
|
|
3
|
-
const identityUndeclaredClaimTrusted = {
|
|
4
|
-
categories: ["SECURITY"],
|
|
5
|
-
description: "An authorization read of `auth.identity.<key>` for a claim outside the declared `defineIdentity({ ... })` contract. `defineIdentity` validates only declared claims and forwards undeclared ones verbatim, so the value the policy trusts was never validated — an attacker's token can forge it.",
|
|
6
|
-
facing: "EXTERNAL",
|
|
7
|
-
level: "WARN",
|
|
8
|
-
name: "identity_undeclared_claim_trusted",
|
|
9
|
-
remediation: "Declare the claim in `defineIdentity({ ... })` (e.g. `role: v.optional(v.string())`) so the worker validates it at the trust boundary before a policy reads it, or stop trusting it for authorization. Declared claims (and `userId`) are validated; undeclared claims are forwarded unchecked.",
|
|
10
|
-
run: (context) => {
|
|
11
|
-
if (context.identityClaimReads === void 0) {
|
|
12
|
-
return [];
|
|
13
|
-
}
|
|
14
|
-
return context.identityClaimReads.filter((row) => !row.declared).map((row) => {
|
|
15
|
-
const location = `\`${row.exportName}\` (${row.file}:${row.line.toString()})`;
|
|
16
|
-
return emit(identityUndeclaredClaimTrusted, {
|
|
17
|
-
cacheKey: `identity_undeclared_claim_trusted:${row.file}:${row.line.toString()}:${row.key}`,
|
|
18
|
-
detail: `\`${row.key}\` in ${location} is read off \`identity\` but is not declared in \`defineIdentity({ ... })\`. Undeclared claims are forwarded unvalidated, so this authorization decision trusts a forgeable value.`,
|
|
19
|
-
metadata: {
|
|
20
|
-
exportName: row.exportName,
|
|
21
|
-
file: row.file,
|
|
22
|
-
key: row.key,
|
|
23
|
-
line: row.line
|
|
24
|
-
}
|
|
25
|
-
});
|
|
26
|
-
});
|
|
27
|
-
},
|
|
28
|
-
source: "static",
|
|
29
|
-
title: "Authorization trusts an undeclared identity claim"
|
|
30
|
-
};
|
|
31
|
-
|
|
32
|
-
export { identityUndeclaredClaimTrusted as default };
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
import { m as makeArgumentDerivedSinkLint } from './argument-derived-sink-C1xTTqAt.mjs';
|
|
2
|
-
|
|
3
|
-
const imagesUrlSourceFromUserInput = makeArgumentDerivedSinkLint({
|
|
4
|
-
cacheKey: (access) => `images_url_source_from_user_input:${access.file}:${access.line.toString()}`,
|
|
5
|
-
categories: ["SECURITY"],
|
|
6
|
-
description: "A `buildImageDeliveryUrl({ key, … })` call's `key` — the CDN transform's source image, an absolute URL or an origin-relative key — is derived from the handler's `args` with no server-side scoping, so any caller can point the CDN's on-the-fly transform at an attacker-chosen origin (SSRF / open proxy) or an arbitrary key under the account's own store.",
|
|
7
|
-
detail: (access) => `\`buildImageDeliveryUrl\` in \`${access.exportName}\` (${access.file}:${access.line.toString()}) builds its delivery URL from a \`key\` derived from \`args\` with no server-side scoping — any caller can point the CDN's on-the-fly transform at an attacker-chosen origin or store key. Validate \`key\` against an allowlist, or derive it from server-trusted state.`,
|
|
8
|
-
facing: "EXTERNAL",
|
|
9
|
-
getAccesses: (context) => context.imageDeliveryUrlAccesses,
|
|
10
|
-
level: "WARN",
|
|
11
|
-
metadata: (access) => {
|
|
12
|
-
return { exportName: access.exportName, file: access.file, line: access.line };
|
|
13
|
-
},
|
|
14
|
-
name: "images_url_source_from_user_input",
|
|
15
|
-
remediation: "Validate `key` against an allowlist of known origins/prefixes before calling `buildImageDeliveryUrl`, or derive it from server-trusted state (a stored record's own image key) rather than passing `args` straight through.",
|
|
16
|
-
title: "Image delivery URL built from unscoped user input"
|
|
17
|
-
});
|
|
18
|
-
|
|
19
|
-
export { imagesUrlSourceFromUserInput as default };
|
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
import { e as emit } from './finding-Dm_zvzS1.mjs';
|
|
2
|
-
import { t as tableColumnSet } from './helpers-BySnKhVB.mjs';
|
|
3
|
-
|
|
4
|
-
const indexReferencesUnknownField = {
|
|
5
|
-
categories: ["SCHEMA"],
|
|
6
|
-
description: "An index references a column that is not declared on its table. The index can never match, and the typo would otherwise surface only at runtime.",
|
|
7
|
-
facing: "INTERNAL",
|
|
8
|
-
level: "ERROR",
|
|
9
|
-
name: "index_references_unknown_field",
|
|
10
|
-
remediation: "Fix the column name in the index declaration, or add the column to the table.",
|
|
11
|
-
run: (context) => {
|
|
12
|
-
const findings = [];
|
|
13
|
-
for (const table of context.schema.tables) {
|
|
14
|
-
const columns = tableColumnSet(table);
|
|
15
|
-
for (const index of table.indexes) {
|
|
16
|
-
for (const field of index.fields) {
|
|
17
|
-
if (columns.has(field)) {
|
|
18
|
-
continue;
|
|
19
|
-
}
|
|
20
|
-
findings.push(
|
|
21
|
-
emit(indexReferencesUnknownField, {
|
|
22
|
-
cacheKey: `index_references_unknown_field:${table.name}:${index.name}:${field}`,
|
|
23
|
-
detail: `Index "${index.name}" on table "${table.name}" references column "${field}", which is not declared on the table.`,
|
|
24
|
-
metadata: { field, index: index.name, indexKind: index.kind, table: table.name }
|
|
25
|
-
})
|
|
26
|
-
);
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
return findings;
|
|
31
|
-
},
|
|
32
|
-
source: "static",
|
|
33
|
-
title: "Index references unknown field"
|
|
34
|
-
};
|
|
35
|
-
|
|
36
|
-
export { indexReferencesUnknownField as default };
|
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
import { e as emit } from './finding-Dm_zvzS1.mjs';
|
|
2
|
-
|
|
3
|
-
const HOT_SCAN_THRESHOLD = 25;
|
|
4
|
-
const indexUtilization = {
|
|
5
|
-
categories: ["PERFORMANCE"],
|
|
6
|
-
description: "Recorded reads show an index the workload doesn't pay for: either a declared index no read ever used (dead overhead on every write) or a table read hot with no index at all (a repeated full scan that degrades as it grows).",
|
|
7
|
-
facing: "INTERNAL",
|
|
8
|
-
level: "INFO",
|
|
9
|
-
name: "index_utilization",
|
|
10
|
-
remediation: "Drop a dead index so writes stop maintaining it; add an index covering the hot full-scanned table's read predicate so the read stops scanning every row.",
|
|
11
|
-
run: (context) => {
|
|
12
|
-
const findings = [];
|
|
13
|
-
for (const hit of context.indexHits ?? []) {
|
|
14
|
-
if (hit.reads > 0) {
|
|
15
|
-
continue;
|
|
16
|
-
}
|
|
17
|
-
findings.push(
|
|
18
|
-
emit(indexUtilization, {
|
|
19
|
-
cacheKey: `index_utilization:dead_index:${hit.table}:${hit.index}`,
|
|
20
|
-
detail: `Index "${hit.index}" on table "${hit.table}" has recorded no reads since its counter was created — it's dead overhead: every write maintains it and nothing reads through it.`,
|
|
21
|
-
metadata: { index: hit.index, kind: "dead_index", reads: hit.reads, table: hit.table }
|
|
22
|
-
})
|
|
23
|
-
);
|
|
24
|
-
}
|
|
25
|
-
for (const scan of context.tableScans ?? []) {
|
|
26
|
-
if (scan.scans < HOT_SCAN_THRESHOLD) {
|
|
27
|
-
continue;
|
|
28
|
-
}
|
|
29
|
-
findings.push(
|
|
30
|
-
emit(indexUtilization, {
|
|
31
|
-
cacheKey: `index_utilization:hot_scan:${scan.table}`,
|
|
32
|
-
detail: `Table "${scan.table}" has been full-scanned ${scan.scans.toString()} times (cumulative) with no index — a repeated full scan that degrades linearly as "${scan.table}" grows. Add an index covering the read predicate.`,
|
|
33
|
-
facing: "EXTERNAL",
|
|
34
|
-
level: "WARN",
|
|
35
|
-
metadata: { kind: "hot_scan", scans: scan.scans, table: scan.table }
|
|
36
|
-
})
|
|
37
|
-
);
|
|
38
|
-
}
|
|
39
|
-
return findings;
|
|
40
|
-
},
|
|
41
|
-
source: "runtime",
|
|
42
|
-
title: "Index utilization"
|
|
43
|
-
};
|
|
44
|
-
|
|
45
|
-
export { indexUtilization as default };
|
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
import { e as emit } from './finding-Dm_zvzS1.mjs';
|
|
2
|
-
|
|
3
|
-
const insertManyUnsafeUserData = {
|
|
4
|
-
categories: ["SECURITY"],
|
|
5
|
-
description: "A public procedure calls `ctx.db.insertManyUnsafe(...)`, which writes rows straight to storage and bypasses both the per-row validators and the schema's insert triggers (server-trusted columns, ownership stamping, RLS write checks). On a public entry point the rows are request-shaped, so a caller can write columns they shouldn't control and skip every trigger-enforced invariant.",
|
|
6
|
-
facing: "EXTERNAL",
|
|
7
|
-
level: "WARN",
|
|
8
|
-
name: "insert_many_unsafe_user_data",
|
|
9
|
-
remediation: "Use the validated `ctx.db.insert(...)` path in public procedures so validators and insert triggers run. If you need the unsafe bulk write, move it into an internal function (`internalMutation`/`internalAction`) that is fed only server-constructed rows, never request input.",
|
|
10
|
-
run: (context) => {
|
|
11
|
-
if (context.procedureProtections === void 0) {
|
|
12
|
-
return [];
|
|
13
|
-
}
|
|
14
|
-
return context.procedureProtections.filter((procedure) => procedure.usesInsertManyUnsafe && procedure.visibility === "public").map(
|
|
15
|
-
(procedure) => emit(insertManyUnsafeUserData, {
|
|
16
|
-
cacheKey: `insert_many_unsafe_user_data:${procedure.file}:${procedure.exportName}`,
|
|
17
|
-
detail: `\`${procedure.exportName}\` (${procedure.file}) is public and calls \`ctx.db.insertManyUnsafe(...)\`, bypassing validators and insert triggers on request-shaped rows. Switch to \`ctx.db.insert(...)\`, or move the bulk write into an internal function fed server-built rows.`,
|
|
18
|
-
metadata: { exportName: procedure.exportName, file: procedure.file, kind: procedure.kind }
|
|
19
|
-
})
|
|
20
|
-
);
|
|
21
|
-
},
|
|
22
|
-
source: "static",
|
|
23
|
-
title: "Public procedure uses insertManyUnsafe (bypasses validators and triggers)"
|
|
24
|
-
};
|
|
25
|
-
|
|
26
|
-
export { insertManyUnsafeUserData as default };
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
import { m as makeArgumentDerivedSinkLint } from './argument-derived-sink-C1xTTqAt.mjs';
|
|
2
|
-
|
|
3
|
-
const kvUnscopedUserKeyIdor = makeArgumentDerivedSinkLint({
|
|
4
|
-
cacheKey: (access) => `kv_unscoped_user_key_idor:${access.file}:${access.line.toString()}`,
|
|
5
|
-
categories: ["SECURITY"],
|
|
6
|
-
description: "A `ctx.kv` read/write uses a namespace key derived from the handler's `args` with no server-side scoping. Workers KV is a flat namespace, so an unscoped key lets any caller read, overwrite, or delete another user's entry — an insecure direct object reference (IDOR).",
|
|
7
|
-
detail: (access) => `\`ctx.kv.${access.method}\` in \`${access.exportName}\` (${access.file}:${access.line.toString()}) uses a KV key derived from \`args\` with no server-side scoping — any caller can read/overwrite/delete another user's entry (IDOR). Prefix the key with a server-trusted identity (e.g. \`\${ctx.auth.userId}:…\`).`,
|
|
8
|
-
facing: "EXTERNAL",
|
|
9
|
-
getAccesses: (context) => context.kvKeyAccesses,
|
|
10
|
-
level: "ERROR",
|
|
11
|
-
metadata: (access) => {
|
|
12
|
-
return { exportName: access.exportName, file: access.file, line: access.line, method: access.method };
|
|
13
|
-
},
|
|
14
|
-
name: "kv_unscoped_user_key_idor",
|
|
15
|
-
remediation: `Prefix every \`ctx.kv\` key with a server-trusted identity (e.g. \`\${ctx.auth.userId}:\${args.id}\`) so a caller can only address their own entries. Never pass request input straight to \`ctx.kv.get\`/\`put\`/\`delete\`.`,
|
|
16
|
-
title: "Possible IDOR from arg-derived unscoped KV key"
|
|
17
|
-
});
|
|
18
|
-
|
|
19
|
-
export { kvUnscopedUserKeyIdor as default };
|