@atlashub/smartstack-cli 4.31.0 → 4.33.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.documentation/commands.html +952 -116
- package/.documentation/index.html +2 -2
- package/.documentation/init.html +358 -174
- package/dist/mcp-entry.mjs +271 -44
- package/dist/mcp-entry.mjs.map +1 -1
- package/package.json +1 -1
- package/templates/mcp-scaffolding/controller.cs.hbs +54 -128
- package/templates/project/README.md +19 -0
- package/templates/skills/apex/SKILL.md +16 -10
- package/templates/skills/apex/_shared.md +1 -1
- package/templates/skills/apex/references/checks/architecture-checks.sh +154 -0
- package/templates/skills/apex/references/checks/backend-checks.sh +194 -0
- package/templates/skills/apex/references/checks/frontend-checks.sh +448 -0
- package/templates/skills/apex/references/checks/infrastructure-checks.sh +255 -0
- package/templates/skills/apex/references/checks/security-checks.sh +153 -0
- package/templates/skills/apex/references/checks/seed-checks.sh +536 -0
- package/templates/skills/apex/references/frontend-route-wiring-app-tsx.md +49 -192
- package/templates/skills/apex/references/parallel-execution.md +18 -5
- package/templates/skills/apex/references/post-checks.md +124 -2156
- package/templates/skills/apex/references/smartstack-api.md +160 -957
- package/templates/skills/apex/references/smartstack-frontend-compliance.md +23 -1
- package/templates/skills/apex/references/smartstack-frontend.md +134 -1022
- package/templates/skills/apex/references/smartstack-layers.md +12 -6
- package/templates/skills/apex/steps/step-00-init.md +81 -238
- package/templates/skills/apex/steps/step-03-execute.md +25 -751
- package/templates/skills/apex/steps/step-03a-layer0-domain.md +118 -0
- package/templates/skills/apex/steps/step-03b-layer1-seed.md +91 -0
- package/templates/skills/apex/steps/step-03c-layer2-backend.md +240 -0
- package/templates/skills/apex/steps/step-03d-layer3-frontend.md +300 -0
- package/templates/skills/apex/steps/step-03e-layer4-devdata.md +44 -0
- package/templates/skills/apex/steps/step-04-examine.md +70 -150
- package/templates/skills/application/references/frontend-i18n-and-output.md +2 -2
- package/templates/skills/application/references/frontend-route-naming.md +5 -1
- package/templates/skills/application/references/frontend-route-wiring-app-tsx.md +49 -198
- package/templates/skills/application/references/frontend-verification.md +11 -11
- package/templates/skills/application/steps/step-05-frontend.md +26 -15
- package/templates/skills/application/templates-frontend.md +4 -0
- package/templates/skills/cli-app-sync/SKILL.md +2 -2
- package/templates/skills/cli-app-sync/references/comparison-map.md +1 -1
- package/templates/skills/controller/references/controller-code-templates.md +70 -67
- package/templates/skills/controller/references/mcp-scaffold-workflow.md +5 -1
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
# POST-CHECK: Infrastructure — Migration & Build
|
|
5
|
+
# C13, C38-C43, C50-C51, C56: Database migrations, DI registration, routing uniqueness
|
|
6
|
+
|
|
7
|
+
FAIL=false
|
|
8
|
+
|
|
9
|
+
# POST-CHECK C13: i18n files must contain required structural keys
|
|
10
|
+
I18N_DIR="src/i18n/locales/fr"
|
|
11
|
+
if [ -d "$I18N_DIR" ]; then
|
|
12
|
+
REQUIRED_KEYS="actions columns empty errors form labels messages validation"
|
|
13
|
+
for JSON_FILE in "$I18N_DIR"/*.json; do
|
|
14
|
+
[ ! -f "$JSON_FILE" ] && continue
|
|
15
|
+
BASENAME=$(basename "$JSON_FILE")
|
|
16
|
+
case "$BASENAME" in common.json|navigation.json) continue;; esac
|
|
17
|
+
for KEY in $REQUIRED_KEYS; do
|
|
18
|
+
if ! jq -e "has(\"$KEY\")" "$JSON_FILE" > /dev/null 2>&1; then
|
|
19
|
+
echo "WARNING: i18n file missing required key '$KEY': $JSON_FILE"
|
|
20
|
+
echo "Module i18n files MUST contain: $REQUIRED_KEYS"
|
|
21
|
+
fi
|
|
22
|
+
done
|
|
23
|
+
done
|
|
24
|
+
fi
|
|
25
|
+
|
|
26
|
+
# POST-CHECK C38: Migration ModelSnapshot must contain ALL entities registered in DbContext (BLOCKING)
|
|
27
|
+
SNAPSHOT=$(find src/ -name "*ModelSnapshot.cs" -path "*/Migrations/*" 2>/dev/null | head -1)
|
|
28
|
+
DBCONTEXT=$(find src/ -name "*DbContext.cs" -path "*/Persistence/*" ! -name "*DesignTime*" 2>/dev/null | head -1)
|
|
29
|
+
if [ -n "$SNAPSHOT" ] && [ -n "$DBCONTEXT" ]; then
|
|
30
|
+
DBSET_ENTITIES=$(grep -oP 'DbSet<(\w+)>' "$DBCONTEXT" 2>/dev/null | grep -oP '<\K\w+(?=>)' | sort -u || true)
|
|
31
|
+
FAIL_C38=false
|
|
32
|
+
for ENTITY in $DBSET_ENTITIES; do
|
|
33
|
+
if echo "$ENTITY" | grep -qP '^(Navigation|Tenant|User|Role|Permission|AuditLog|ApplicationTracking)'; then
|
|
34
|
+
continue
|
|
35
|
+
fi
|
|
36
|
+
if ! grep -q "Entity<$ENTITY>" "$SNAPSHOT" 2>/dev/null; then
|
|
37
|
+
echo "BLOCKING: Entity '$ENTITY' is registered as DbSet in $DBCONTEXT but MISSING from ModelSnapshot"
|
|
38
|
+
echo " This means no migration was created for this entity — it will not exist in the database."
|
|
39
|
+
echo " Fix: Run 'dotnet ef migrations add' to include all new entities"
|
|
40
|
+
FAIL_C38=true
|
|
41
|
+
fi
|
|
42
|
+
done
|
|
43
|
+
if [ "$FAIL_C38" = true ]; then
|
|
44
|
+
echo ""
|
|
45
|
+
echo " Root cause: Migration was likely created once for the first batch of entities,"
|
|
46
|
+
echo " but additional entities were added later without regenerating the migration."
|
|
47
|
+
echo " Fix: Create a new migration that covers ALL missing entities."
|
|
48
|
+
FAIL=true
|
|
49
|
+
fi
|
|
50
|
+
fi
|
|
51
|
+
|
|
52
|
+
# POST-CHECK C39: I18n namespace files must be registered in i18n config (CRITICAL)
|
|
53
|
+
I18N_CONFIG=$(find src/ web/ -path "*/i18n/config.ts" -o -path "*/i18n/index.ts" -o -path "*/i18n/i18n.ts" 2>/dev/null | grep -v node_modules | head -1)
|
|
54
|
+
if [ -n "$I18N_CONFIG" ]; then
|
|
55
|
+
FR_FILES=$(find src/ web/ -path "*/i18n/locales/fr/*.json" 2>/dev/null | grep -v node_modules | grep -v common.json | grep -v navigation.json || true)
|
|
56
|
+
if [ -n "$FR_FILES" ]; then
|
|
57
|
+
FAIL_C39=false
|
|
58
|
+
for JSON_FILE in $FR_FILES; do
|
|
59
|
+
NS=$(basename "$JSON_FILE" .json)
|
|
60
|
+
if ! grep -q "$NS" "$I18N_CONFIG" 2>/dev/null; then
|
|
61
|
+
echo "CRITICAL: i18n namespace '$NS' (from $JSON_FILE) is not registered in $I18N_CONFIG"
|
|
62
|
+
echo " Pages using useTranslation(['$NS']) will get empty translations at runtime"
|
|
63
|
+
echo " Fix: Add '$NS' to the resources/ns configuration in $I18N_CONFIG"
|
|
64
|
+
FAIL_C39=true
|
|
65
|
+
fi
|
|
66
|
+
done
|
|
67
|
+
if [ "$FAIL_C39" = true ]; then
|
|
68
|
+
FAIL=true
|
|
69
|
+
fi
|
|
70
|
+
fi
|
|
71
|
+
fi
|
|
72
|
+
|
|
73
|
+
# POST-CHECK C40: FluentValidation validators must be registered via DI (BLOCKING)
|
|
74
|
+
VALIDATOR_FILES=$(find src/ -name "*Validator.cs" -path "*/Validators/*" 2>/dev/null | grep -v test | grep -v Test)
|
|
75
|
+
if [ -n "$VALIDATOR_FILES" ]; then
|
|
76
|
+
DI_FILE=$(find src/ -name "DependencyInjection.cs" -o -name "ServiceCollectionExtensions.cs" 2>/dev/null | grep -v test | head -1)
|
|
77
|
+
if [ -z "$DI_FILE" ]; then
|
|
78
|
+
echo "BLOCKING: Validators exist but no DependencyInjection.cs found for DI registration"
|
|
79
|
+
FAIL=true
|
|
80
|
+
else
|
|
81
|
+
HAS_ASSEMBLY_REG=$(grep -c "AddValidatorsFromAssembly\|AddValidatorsFromAssemblyContaining" "$DI_FILE" 2>/dev/null || true)
|
|
82
|
+
if [ "$HAS_ASSEMBLY_REG" -eq 0 ]; then
|
|
83
|
+
VALIDATOR_COUNT=$(echo "$VALIDATOR_FILES" | wc -l)
|
|
84
|
+
REGISTERED_COUNT=0
|
|
85
|
+
for VF in $VALIDATOR_FILES; do
|
|
86
|
+
VN=$(basename "$VF" .cs)
|
|
87
|
+
if grep -q "$VN" "$DI_FILE" 2>/dev/null; then
|
|
88
|
+
REGISTERED_COUNT=$((REGISTERED_COUNT + 1))
|
|
89
|
+
fi
|
|
90
|
+
done
|
|
91
|
+
if [ "$REGISTERED_COUNT" -eq 0 ]; then
|
|
92
|
+
echo "BLOCKING: $VALIDATOR_COUNT validators exist but NONE are registered in DI ($DI_FILE)"
|
|
93
|
+
echo " Fix: Add 'services.AddValidatorsFromAssemblyContaining<Create{Entity}DtoValidator>();' to $DI_FILE"
|
|
94
|
+
echo " Or use 'services.AddValidatorsFromAssembly(typeof(Create{Entity}DtoValidator).Assembly);'"
|
|
95
|
+
FAIL=true
|
|
96
|
+
fi
|
|
97
|
+
fi
|
|
98
|
+
fi
|
|
99
|
+
fi
|
|
100
|
+
|
|
101
|
+
# POST-CHECK C41: Date/date properties in DTOs must use DateOnly, not string (WARNING)
|
|
102
|
+
DTO_FILES=$(find src/ -name "*Dto.cs" -path "*/DTOs/*" 2>/dev/null)
|
|
103
|
+
if [ -n "$DTO_FILES" ]; then
|
|
104
|
+
FAIL_C41=false
|
|
105
|
+
for f in $DTO_FILES; do
|
|
106
|
+
BAD_DATES=$(grep -Pn 'string\??\s+\w*[Dd]ate\w*\s*[{;,]' "$f" 2>/dev/null | grep -vi "Updated\|Created\|format\|pattern\|string\|parse" || true)
|
|
107
|
+
if [ -n "$BAD_DATES" ]; then
|
|
108
|
+
echo "WARNING: DTO has string type for date field — must use DateOnly: $f"
|
|
109
|
+
echo "$BAD_DATES"
|
|
110
|
+
echo " Fix: Change 'string Date' to 'DateOnly Date' (or 'DateOnly? Date' if nullable)"
|
|
111
|
+
echo " DateOnly is the correct .NET type for date-only values (no time component)"
|
|
112
|
+
fi
|
|
113
|
+
done
|
|
114
|
+
fi
|
|
115
|
+
|
|
116
|
+
# POST-CHECK C42: Every module with entities must have a migration covering them (BLOCKING)
|
|
117
|
+
ENTITY_FILES=$(find src/ -path "*/Domain/Entities/*" -name "*.cs" 2>/dev/null | grep -v test)
|
|
118
|
+
MIGRATION_DIR=$(find src/ -path "*/Migrations" -type d 2>/dev/null | head -1)
|
|
119
|
+
if [ -n "$ENTITY_FILES" ] && [ -n "$MIGRATION_DIR" ]; then
|
|
120
|
+
MIGRATION_FILES=$(find "$MIGRATION_DIR" -name "*.cs" ! -name "*ModelSnapshot*" ! -name "*DesignTime*" 2>/dev/null)
|
|
121
|
+
if [ -z "$MIGRATION_FILES" ]; then
|
|
122
|
+
echo "BLOCKING: Entity files exist in Domain/Entities but NO migration files found in $MIGRATION_DIR"
|
|
123
|
+
FAIL=true
|
|
124
|
+
else
|
|
125
|
+
FAIL_C42=false
|
|
126
|
+
for EF in $ENTITY_FILES; do
|
|
127
|
+
ENTITY_NAME=$(basename "$EF" .cs)
|
|
128
|
+
if grep -qP '^\s*(public\s+)?(abstract|interface)\s' "$EF" 2>/dev/null; then continue; fi
|
|
129
|
+
FOUND=$(grep -l "$ENTITY_NAME" $MIGRATION_FILES 2>/dev/null || true)
|
|
130
|
+
if [ -z "$FOUND" ]; then
|
|
131
|
+
echo "BLOCKING: Entity '$ENTITY_NAME' ($EF) not found in any migration file"
|
|
132
|
+
echo " This entity will NOT have a database table."
|
|
133
|
+
echo " Fix: Run 'dotnet ef migrations add' to create a migration covering this entity"
|
|
134
|
+
FAIL_C42=true
|
|
135
|
+
fi
|
|
136
|
+
done
|
|
137
|
+
if [ "$FAIL_C42" = true ]; then
|
|
138
|
+
FAIL=true
|
|
139
|
+
fi
|
|
140
|
+
fi
|
|
141
|
+
fi
|
|
142
|
+
|
|
143
|
+
# POST-CHECK C43: Controllers must NOT have both [Route] and [NavRoute] attributes (BLOCKING)
|
|
144
|
+
CTRL_FILES=$(find src/ -path "*/Controllers/*" -name "*Controller.cs" 2>/dev/null)
|
|
145
|
+
if [ -n "$CTRL_FILES" ]; then
|
|
146
|
+
FAIL_C43=false
|
|
147
|
+
for f in $CTRL_FILES; do
|
|
148
|
+
HAS_NAVROUTE=$(grep -c '\[NavRoute(' "$f" 2>/dev/null || true)
|
|
149
|
+
HAS_ROUTE=$(grep -c '\[Route(' "$f" 2>/dev/null || true)
|
|
150
|
+
if [ "$HAS_NAVROUTE" -gt 0 ] && [ "$HAS_ROUTE" -gt 0 ]; then
|
|
151
|
+
NAVROUTE_VAL=$(grep -oP 'NavRoute\("([^"]+)"' "$f" 2>/dev/null | head -1)
|
|
152
|
+
ROUTE_VAL=$(grep -oP 'Route\("([^"]+)"' "$f" 2>/dev/null | head -1)
|
|
153
|
+
echo "BLOCKING: Controller has BOTH [Route] and [NavRoute] — remove [Route]: $f"
|
|
154
|
+
echo " Found: [$ROUTE_VAL] + [$NAVROUTE_VAL]"
|
|
155
|
+
echo " In SmartStack, [NavRoute] resolves routes dynamically from the database."
|
|
156
|
+
echo " Having [Route] alongside it causes route conflicts and 404s."
|
|
157
|
+
echo " Fix: Remove the [Route(...)] attribute, keep only [NavRoute(...)]"
|
|
158
|
+
FAIL_C43=true
|
|
159
|
+
fi
|
|
160
|
+
done
|
|
161
|
+
if [ "$FAIL_C43" = true ]; then
|
|
162
|
+
FAIL=true
|
|
163
|
+
fi
|
|
164
|
+
fi
|
|
165
|
+
|
|
166
|
+
# POST-CHECK C50: NavRoute Uniqueness — no duplicate NavRoute values (BLOCKING)
|
|
167
|
+
CTRL_FILES=$(find src/ -path "*/Controllers/*" -name "*Controller.cs" 2>/dev/null)
|
|
168
|
+
if [ -n "$CTRL_FILES" ]; then
|
|
169
|
+
NAVROUTES=$(grep -Pn '\[NavRoute\("([^"]+)"\)' $CTRL_FILES 2>/dev/null | sed 's/.*\[NavRoute("\([^"]*\)").*/\1/' | sort || true)
|
|
170
|
+
DUPLICATES=$(echo "$NAVROUTES" | uniq -d || true)
|
|
171
|
+
if [ -n "$DUPLICATES" ]; then
|
|
172
|
+
echo "BLOCKING: Duplicate NavRoute values detected:"
|
|
173
|
+
for DUP in $DUPLICATES; do
|
|
174
|
+
echo " NavRoute: $DUP"
|
|
175
|
+
grep -l "\[NavRoute(\"$DUP\")\]" $CTRL_FILES 2>/dev/null | while read -r f; do
|
|
176
|
+
echo " - $f"
|
|
177
|
+
done
|
|
178
|
+
done
|
|
179
|
+
echo " Fix: Each controller MUST have a unique NavRoute value"
|
|
180
|
+
FAIL=true
|
|
181
|
+
fi
|
|
182
|
+
fi
|
|
183
|
+
|
|
184
|
+
# POST-CHECK C51: NavRoute Segments vs Controller Hierarchy (WARNING)
|
|
185
|
+
CTRL_FILES=$(find src/ -path "*/Controllers/*" -name "*Controller.cs" 2>/dev/null)
|
|
186
|
+
if [ -n "$CTRL_FILES" ]; then
|
|
187
|
+
for f in $CTRL_FILES; do
|
|
188
|
+
NAVROUTE=$(grep -oP '\[NavRoute\("\K[^"]+' "$f")
|
|
189
|
+
if [ -z "$NAVROUTE" ]; then continue; fi
|
|
190
|
+
|
|
191
|
+
DOTS=$(echo "$NAVROUTE" | tr -cd '.' | wc -c)
|
|
192
|
+
|
|
193
|
+
REL_PATH=$(echo "$f" | sed 's|.*/Controllers/||')
|
|
194
|
+
DIR_DEPTH=$(echo "$REL_PATH" | tr '/' '\n' | wc -l)
|
|
195
|
+
|
|
196
|
+
if [ "$DIR_DEPTH" -ge 3 ] && [ "$DOTS" -le 1 ]; then
|
|
197
|
+
echo "WARNING: Controller in section subfolder but NavRoute has only $((DOTS+1)) segments"
|
|
198
|
+
echo " File: $f"
|
|
199
|
+
echo " NavRoute: $NAVROUTE"
|
|
200
|
+
echo " Expected: 3+ segments (app.module.section) for controllers in section subfolders"
|
|
201
|
+
echo " Fix: Update NavRoute to include the module segment (e.g., 'app.module.section')"
|
|
202
|
+
fi
|
|
203
|
+
|
|
204
|
+
if [ "$DOTS" -eq 0 ]; then
|
|
205
|
+
echo "BLOCKING: NavRoute '$NAVROUTE' has only 1 segment (minimum 2 required): $f"
|
|
206
|
+
FAIL=true
|
|
207
|
+
fi
|
|
208
|
+
done
|
|
209
|
+
fi
|
|
210
|
+
|
|
211
|
+
# POST-CHECK C56: Hierarchy Artifact Completeness — current run only (BLOCKING)
|
|
212
|
+
# REQUIRES: {entities} and {sections} from current apex run context
|
|
213
|
+
# If not available, skip this check (cannot determine scope)
|
|
214
|
+
|
|
215
|
+
if [ -n "${ENTITIES:-}" ] && [ -n "${SECTIONS:-}" ]; then
|
|
216
|
+
FAIL_C56=false
|
|
217
|
+
|
|
218
|
+
for ENTITY in $ENTITIES; do
|
|
219
|
+
CTRL=$(find src/ -path "*/Controllers/*" -name "${ENTITY}*Controller.cs" 2>/dev/null | head -1)
|
|
220
|
+
if [ -z "$CTRL" ]; then
|
|
221
|
+
echo "BLOCKING: Entity '$ENTITY' has no controller file"
|
|
222
|
+
FAIL_C56=true
|
|
223
|
+
fi
|
|
224
|
+
if ! grep -q "\[NavRoute" "$CTRL" 2>/dev/null; then
|
|
225
|
+
echo "BLOCKING: Controller for '$ENTITY' ($CTRL) missing [NavRoute] attribute"
|
|
226
|
+
echo "SmartStack convention: [NavRoute] is the ONLY route attribute needed"
|
|
227
|
+
FAIL_C56=true
|
|
228
|
+
fi
|
|
229
|
+
if grep -q "\[Route(" "$CTRL" 2>/dev/null && grep -q "\[NavRoute" "$CTRL" 2>/dev/null; then
|
|
230
|
+
echo "BLOCKING: Controller $CTRL has BOTH [Route] and [NavRoute] (see C43)"
|
|
231
|
+
FAIL_C56=true
|
|
232
|
+
fi
|
|
233
|
+
done
|
|
234
|
+
|
|
235
|
+
PERM_SEED=$(find src/ -path "*/Seeding/Data/*" -name "*PermissionsSeedData.cs" 2>/dev/null)
|
|
236
|
+
NAV_SEED=$(find src/ -path "*/Seeding/Data/*" -name "*NavigationSeedData.cs" ! -name "*Application*" 2>/dev/null)
|
|
237
|
+
for SECTION in $SECTIONS; do
|
|
238
|
+
if [ -n "$PERM_SEED" ] && ! grep -q "\"$SECTION\"" $PERM_SEED 2>/dev/null; then
|
|
239
|
+
echo "BLOCKING: Section '$SECTION' missing from PermissionsSeedData"
|
|
240
|
+
FAIL_C56=true
|
|
241
|
+
fi
|
|
242
|
+
if [ -n "$NAV_SEED" ] && ! grep -q "\"$SECTION\"" $NAV_SEED 2>/dev/null; then
|
|
243
|
+
echo "BLOCKING: Section '$SECTION' missing from NavigationSeedData"
|
|
244
|
+
FAIL_C56=true
|
|
245
|
+
fi
|
|
246
|
+
done
|
|
247
|
+
|
|
248
|
+
if [ "$FAIL_C56" = true ]; then
|
|
249
|
+
FAIL=true
|
|
250
|
+
fi
|
|
251
|
+
fi
|
|
252
|
+
|
|
253
|
+
if [ "$FAIL" = true ]; then
|
|
254
|
+
exit 1
|
|
255
|
+
fi
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
# POST-CHECK: Security — Tenant Isolation & Authorization (defense-in-depth with MCP)
|
|
5
|
+
# S1-S9: Blocks multi-tenant data leakage, RBAC bypass, and authorization errors
|
|
6
|
+
|
|
7
|
+
FAIL=false
|
|
8
|
+
|
|
9
|
+
# POST-CHECK S1: All services must filter by TenantId (OWASP A01)
|
|
10
|
+
SERVICE_FILES=$(find src/ -path "*/Services/*" -name "*Service.cs" ! -name "I*Service.cs" 2>/dev/null)
|
|
11
|
+
if [ -n "$SERVICE_FILES" ]; then
|
|
12
|
+
for f in $SERVICE_FILES; do
|
|
13
|
+
HAS_TENANT_FILTER=$(grep -c "TenantId" "$f")
|
|
14
|
+
HAS_OPTIONAL_ENTITY=false
|
|
15
|
+
if grep -q "IOptionalTenantEntity\|IScopedTenantEntity" "$f"; then
|
|
16
|
+
HAS_OPTIONAL_ENTITY=true
|
|
17
|
+
fi
|
|
18
|
+
if [ "$HAS_TENANT_FILTER" -eq 0 ] && [ "$HAS_OPTIONAL_ENTITY" = false ]; then
|
|
19
|
+
echo "BLOCKING (OWASP A01): Service missing TenantId filter or optional tenant entity: $f"
|
|
20
|
+
echo "Every service query MUST filter by _currentTenant.TenantId"
|
|
21
|
+
FAIL=true
|
|
22
|
+
fi
|
|
23
|
+
if grep -q "Guid.Empty" "$f"; then
|
|
24
|
+
echo "BLOCKING (OWASP A01): Service uses Guid.Empty instead of _currentTenant.TenantId: $f"
|
|
25
|
+
FAIL=true
|
|
26
|
+
fi
|
|
27
|
+
done
|
|
28
|
+
fi
|
|
29
|
+
|
|
30
|
+
# POST-CHECK S2: Controllers must use [RequirePermission], not just [Authorize] (BLOCKING)
|
|
31
|
+
CTRL_FILES=$(find src/ -path "*/Controllers/*" -name "*Controller.cs" 2>/dev/null)
|
|
32
|
+
if [ -n "$CTRL_FILES" ]; then
|
|
33
|
+
for f in $CTRL_FILES; do
|
|
34
|
+
if grep -q "\[Authorize\]" "$f" && ! grep -q "\[RequirePermission" "$f"; then
|
|
35
|
+
echo "BLOCKING: Controller uses [Authorize] without [RequirePermission]: $f"
|
|
36
|
+
echo "[Authorize] alone provides NO RBAC enforcement — any authenticated user has access"
|
|
37
|
+
echo "Fix: Add [RequirePermission(Permissions.{Module}.{Action})] on each endpoint"
|
|
38
|
+
FAIL=true
|
|
39
|
+
fi
|
|
40
|
+
done
|
|
41
|
+
fi
|
|
42
|
+
|
|
43
|
+
# POST-CHECK S3: Services must inject ICurrentTenantService (tenant isolation)
|
|
44
|
+
SERVICE_FILES=$(find src/ -path "*/Services/*" -name "*Service.cs" ! -name "I*Service.cs" 2>/dev/null)
|
|
45
|
+
if [ -n "$SERVICE_FILES" ]; then
|
|
46
|
+
for f in $SERVICE_FILES; do
|
|
47
|
+
if ! grep -qE "ICurrentTenantService|ICurrentUser" "$f"; then
|
|
48
|
+
echo "BLOCKING: Service missing tenant context injection: $f"
|
|
49
|
+
echo "All services MUST inject ICurrentTenantService for tenant isolation"
|
|
50
|
+
FAIL=true
|
|
51
|
+
fi
|
|
52
|
+
done
|
|
53
|
+
fi
|
|
54
|
+
|
|
55
|
+
# POST-CHECK S4: HasQueryFilter must not use Guid.Empty (OWASP A01)
|
|
56
|
+
CONFIG_FILES=$(find src/ -path "*/Configurations/*" -name "*Configuration.cs" 2>/dev/null)
|
|
57
|
+
if [ -n "$CONFIG_FILES" ]; then
|
|
58
|
+
BAD_FILTER=$(grep -Pn 'HasQueryFilter.*Guid\.Empty' $CONFIG_FILES 2>/dev/null)
|
|
59
|
+
if [ -n "$BAD_FILTER" ]; then
|
|
60
|
+
echo "BLOCKING (OWASP A01): HasQueryFilter uses Guid.Empty — bypasses tenant isolation: $BAD_FILTER"
|
|
61
|
+
echo "The EF global query filter MUST use the injected TenantId, not Guid.Empty"
|
|
62
|
+
FAIL=true
|
|
63
|
+
fi
|
|
64
|
+
fi
|
|
65
|
+
|
|
66
|
+
# POST-CHECK S5: Services must NOT use TenantId!.Value (null-forgiving crash pattern)
|
|
67
|
+
SERVICE_FILES=$(find src/ -path "*/Services/*" -name "*Service.cs" ! -name "I*Service.cs" 2>/dev/null)
|
|
68
|
+
if [ -n "$SERVICE_FILES" ]; then
|
|
69
|
+
BAD_PATTERN=$(grep -Pn 'TenantId!\.' $SERVICE_FILES 2>/dev/null)
|
|
70
|
+
if [ -n "$BAD_PATTERN" ]; then
|
|
71
|
+
echo "BLOCKING: TenantId!.Value (null-forgiving) detected — NullReferenceException risk: $BAD_PATTERN"
|
|
72
|
+
echo "Fix: Use guard clause: var tenantId = _currentTenant.TenantId ?? throw new TenantContextRequiredException();"
|
|
73
|
+
FAIL=true
|
|
74
|
+
fi
|
|
75
|
+
fi
|
|
76
|
+
|
|
77
|
+
# POST-CHECK S6: Pages must use lazy loading (no static page imports in routes)
|
|
78
|
+
APP_TSX=$(find src/ -name "App.tsx" -not -path "*/node_modules/*" 2>/dev/null | head -1)
|
|
79
|
+
ROUTE_FILES=$(find src/routes/ -name "*.tsx" -o -name "*.ts" 2>/dev/null)
|
|
80
|
+
if [ -n "$APP_TSX" ]; then
|
|
81
|
+
STATIC_IMPORTS=$(grep -Pn "^import .+ from '@/pages/" "$APP_TSX" $ROUTE_FILES 2>/dev/null)
|
|
82
|
+
if [ -n "$STATIC_IMPORTS" ]; then
|
|
83
|
+
echo "BLOCKING: Static page imports in route files — MUST use React.lazy()"
|
|
84
|
+
echo "Static imports load ALL pages upfront, killing initial load performance"
|
|
85
|
+
echo "$STATIC_IMPORTS"
|
|
86
|
+
echo "Fix: const Page = lazy(() => import('@/pages/...').then(m => ({ default: m.Page })))"
|
|
87
|
+
FAIL=true
|
|
88
|
+
fi
|
|
89
|
+
fi
|
|
90
|
+
|
|
91
|
+
# POST-CHECK S7: Controllers must NOT use Guid.Empty for tenantId/userId (OWASP A01)
|
|
92
|
+
CTRL_FILES=$(find src/ -path "*/Controllers/*" -name "*Controller.cs" 2>/dev/null)
|
|
93
|
+
if [ -n "$CTRL_FILES" ]; then
|
|
94
|
+
BAD_GUID=$(grep -Pn 'Guid\.Empty' $CTRL_FILES 2>/dev/null)
|
|
95
|
+
if [ -n "$BAD_GUID" ]; then
|
|
96
|
+
echo "BLOCKING (OWASP A01): Controller uses Guid.Empty — tenant isolation bypassed"
|
|
97
|
+
echo "$BAD_GUID"
|
|
98
|
+
echo "Fix: Use _currentTenant.TenantId from ICurrentTenantService"
|
|
99
|
+
FAIL=true
|
|
100
|
+
fi
|
|
101
|
+
fi
|
|
102
|
+
|
|
103
|
+
# POST-CHECK S8: Write endpoints must NOT use Read permissions (BLOCKING)
|
|
104
|
+
CTRL_FILES=$(find src/ -path "*/Controllers/*" -name "*Controller.cs" 2>/dev/null)
|
|
105
|
+
if [ -n "$CTRL_FILES" ]; then
|
|
106
|
+
for f in $CTRL_FILES; do
|
|
107
|
+
WRITE_WITH_READ=$(grep -Pn '\[(HttpPost|HttpPut|HttpDelete|HttpPatch)' "$f" | while read -r line; do
|
|
108
|
+
LINE_NUM=$(echo "$line" | cut -d: -f1)
|
|
109
|
+
sed -n "$((LINE_NUM-2)),$((LINE_NUM+5))p" "$f" | grep -P 'RequirePermission.*\.Read\b' | head -1
|
|
110
|
+
done)
|
|
111
|
+
if [ -n "$WRITE_WITH_READ" ]; then
|
|
112
|
+
echo "BLOCKING: Write endpoint uses Read permission in $f"
|
|
113
|
+
echo "$WRITE_WITH_READ"
|
|
114
|
+
echo "Fix: POST/PUT/DELETE/PATCH endpoints must use Create/Update/Delete permissions, never Read"
|
|
115
|
+
FAIL=true
|
|
116
|
+
fi
|
|
117
|
+
done
|
|
118
|
+
fi
|
|
119
|
+
|
|
120
|
+
# POST-CHECK S9: FK relationships must enforce tenant isolation (BLOCKING)
|
|
121
|
+
CONFIG_FILES=$(find src/ -path "*/Configurations/*" -name "*Configuration.cs" 2>/dev/null)
|
|
122
|
+
if [ -n "$CONFIG_FILES" ]; then
|
|
123
|
+
for f in $CONFIG_FILES; do
|
|
124
|
+
ENTITY_NAME=$(grep -oP 'IEntityTypeConfiguration<(\w+)>' "$f" | grep -oP '<\K[^>]+')
|
|
125
|
+
if [ -z "$ENTITY_NAME" ]; then continue; fi
|
|
126
|
+
|
|
127
|
+
ENTITY_FILE=$(find src/ -path "*/Domain/*" -name "${ENTITY_NAME}.cs" 2>/dev/null | head -1)
|
|
128
|
+
if [ -z "$ENTITY_FILE" ]; then continue; fi
|
|
129
|
+
|
|
130
|
+
IS_TENANT=$(grep -cE 'ITenantEntity|BaseEntity|TenantId' "$ENTITY_FILE")
|
|
131
|
+
if [ "$IS_TENANT" -eq 0 ]; then continue; fi
|
|
132
|
+
|
|
133
|
+
FK_TARGETS=$(grep -oP 'HasOne<(\w+)>|HasOne\(e\s*=>\s*e\.(\w+)\)' "$f" | grep -oP '\w+(?=>|\))' | sort -u)
|
|
134
|
+
for TARGET in $FK_TARGETS; do
|
|
135
|
+
TARGET_FILE=$(find src/ -path "*/Domain/*" -name "${TARGET}.cs" 2>/dev/null | head -1)
|
|
136
|
+
if [ -n "$TARGET_FILE" ]; then
|
|
137
|
+
TARGET_IS_TENANT=$(grep -cE 'ITenantEntity|BaseEntity|TenantId' "$TARGET_FILE")
|
|
138
|
+
if [ "$TARGET_IS_TENANT" -gt 0 ]; then
|
|
139
|
+
HAS_QUERY_FILTER=$(grep -c 'HasQueryFilter' "$f")
|
|
140
|
+
if [ "$HAS_QUERY_FILTER" -eq 0 ]; then
|
|
141
|
+
echo "WARNING: $f — FK from tenant entity $ENTITY_NAME to tenant entity $TARGET without HasQueryFilter"
|
|
142
|
+
echo "Cross-tenant data leakage possible if FK is assigned without tenant validation"
|
|
143
|
+
echo "Fix: Add tenant validation in service layer before assigning FK, or use composite FK (TenantId + EntityId)"
|
|
144
|
+
fi
|
|
145
|
+
fi
|
|
146
|
+
fi
|
|
147
|
+
done
|
|
148
|
+
done
|
|
149
|
+
fi
|
|
150
|
+
|
|
151
|
+
if [ "$FAIL" = true ]; then
|
|
152
|
+
exit 1
|
|
153
|
+
fi
|