Skip to content

Commit b268a6d

Browse files
Merge pull request #294 from contentstack/feat/Asset-Scanning
Feature branch for Asset scanning v2
2 parents cc5443e + f06ba8f commit b268a6d

70 files changed

Lines changed: 1739 additions & 420 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎.talismanrc‎

Lines changed: 11 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,13 @@
11
fileignoreconfig:
2+
23
- filename: pnpm-lock.yaml
3-
checksum: 693eb73aae594f391247f2feecc55b84447cd39658b6f70f25ef87df19b5c134
4-
- filename: packages/contentstack-export/src/utils/export-config-handler.ts
5-
checksum: 3ff8e8ea60f92311f8224bce457f7aaab026194de61e13eebdc6ca5a39f66bc9
6-
- filename: packages/contentstack-asset-management/src/utils/export-helpers.ts
7-
checksum: 726d1632110ebc203ce70dc1cbe7d4b3011f56349ed371b6645d0375e7818cad
8-
- filename: packages/contentstack-asset-management/src/utils/cs-assets-api-adapter.ts
9-
checksum: bc4a53f96be6a10786e00133245c7bdc43c965c8a98b753e3879e1110cf9c601
10-
- filename: packages/contentstack-asset-management/test/unit/utils/cs-assets-api-adapter.test.ts
11-
checksum: 63c6bff4d51842d8fa3cce88545259d0a2c3cfe71df95d303d993f692cee883b
12-
- filename: packages/contentstack-migration/src/commands/cm/stacks/migration.ts
13-
checksum: 529431473623cf6bbefb35bc898be2a60dcb453e0c9273ecb97923de658769b3
14-
- filename: packages/contentstack-seed/src/commands/cm/stacks/seed.ts
15-
checksum: 9a3fd45a57c6ebf63d6c154657f2ebf799befbbacb9a255944c2d89ae89b22ed
16-
- filename: packages/contentstack-seed/test/commands/cm/stacks/seed.test.ts
17-
checksum: b54f572a614e5d80f7fa27a51e679147cb537fd76ac922eed981d1156a261e50
18-
version: '1.0'
4+
checksum: 649aae748e3ae8c157439dc6c7c9ea402cc19b4dab7ca64b7e4d839d1375df53
5+
- filename: packages/contentstack-audit/src/audit-base-command.ts
6+
checksum: b706ea114a3cb5e4c0e7e163f32ceffa3356a6b0766bf5b8fe387c3c72f44c36
7+
- filename: packages/contentstack-audit/src/types/content-types.ts
8+
checksum: d16a65415c3184f15a807d58e5858310aeb5633794fc9075e36f89c94da636c3
9+
- filename: packages/contentstack-audit/src/modules/entries.ts
10+
checksum: 13072df0872f8c098d23f49b02f9e8f3e492ab2f483d67a5ea5e423ab406ad94
11+
- filename: packages/contentstack-audit/test/unit/modules/entries.test.ts
12+
checksum: 31d9767d87e4e9db54f641e8cb9d3360f591acdaa8b089a83eaa4aacdf0c3a35
13+
version: '1.0'

‎packages/contentstack-apps-cli/package.json‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,8 @@
2525
"@contentstack/cli-command": "~2.0.0-beta.11",
2626
"@contentstack/cli-launch": "^1.11.1",
2727
"@contentstack/cli-utilities": "~2.0.0-beta.12",
28-
"adm-zip": "^0.5.18",
2928
"chalk": "^5.6.2",
29+
"adm-zip": "^0.6.0",
3030
"lodash": "^4.18.1",
3131
"shelljs": "^0.10.0",
3232
"tmp": "^0.2.7",
@@ -110,4 +110,4 @@
110110
"app:deploy": "APDP"
111111
}
112112
}
113-
}
113+
}

‎packages/contentstack-audit/README.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ $ npm install -g @contentstack/cli-audit
1919
$ csdx COMMAND
2020
running command...
2121
$ csdx (--version|-v)
22-
@contentstack/cli-audit/2.0.0-beta.15 darwin-arm64 node-v24.14.0
22+
@contentstack/cli-audit/1.21.0 darwin-arm64 node-v24.18.0
2323
$ csdx --help [COMMAND]
2424
USAGE
2525
$ csdx COMMAND

‎packages/contentstack-audit/package.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,4 +80,4 @@
8080
"keywords": [
8181
"oclif"
8282
]
83-
}
83+
}

‎packages/contentstack-audit/src/audit-base-command.ts‎

Lines changed: 44 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import {
1515
clearProgressModuleSetting,
1616
readContentTypeSchemas,
1717
readGlobalFieldSchemas,
18-
generateUid
18+
generateUid,
1919
} from '@contentstack/cli-utilities';
2020
import { createWriteStream, existsSync, mkdirSync, writeFileSync, rmSync } from 'fs';
2121
import config from './config';
@@ -56,7 +56,9 @@ export abstract class AuditBaseCommand extends BaseCommand<typeof AuditBaseComma
5656
minWidth: 7,
5757
header: 'Fix Status',
5858
get: (row: any) => {
59-
return row.fixStatus === 'Fixed' ? getChalk().greenBright(row.fixStatus) : getChalk().redBright(row.fixStatus);
59+
return row.fixStatus === 'Fixed'
60+
? getChalk().greenBright(row.fixStatus)
61+
: getChalk().redBright(row.fixStatus);
6062
},
6163
},
6264
};
@@ -104,6 +106,8 @@ export abstract class AuditBaseCommand extends BaseCommand<typeof AuditBaseComma
104106
missingFieldRules,
105107
missingMultipleFields,
106108
missingRefsInComposableStudio,
109+
missingScanStatusAssets,
110+
missingAssetRefsInEntries,
107111
} = await this.scanAndFix();
108112

109113
if (this.flags['show-console-output']) {
@@ -135,6 +139,12 @@ export abstract class AuditBaseCommand extends BaseCommand<typeof AuditBaseComma
135139
{ module: 'Entries Changed Multiple Fields', missingRefs: missingMultipleFields },
136140
]);
137141
this.showOutputOnScreenWorkflowsAndExtension([{ module: 'Studio', missingRefs: missingRefsInComposableStudio }]);
142+
this.showOutputOnScreenWorkflowsAndExtension([
143+
{ module: 'Assets Scan Status', missingRefs: missingScanStatusAssets },
144+
]);
145+
this.showOutputOnScreenWorkflowsAndExtension([
146+
{ module: 'Entries Asset Field', missingRefs: missingAssetRefsInEntries },
147+
]);
138148
}
139149
this.showOutputOnScreenWorkflowsAndExtension([{ module: 'Summary', missingRefs: this.summaryDataToPrint }]);
140150

@@ -151,7 +161,9 @@ export abstract class AuditBaseCommand extends BaseCommand<typeof AuditBaseComma
151161
!isEmpty(missingEnvLocalesInEntries) ||
152162
!isEmpty(missingFieldRules) ||
153163
!isEmpty(missingMultipleFields) ||
154-
!isEmpty(missingRefsInComposableStudio)
164+
!isEmpty(missingRefsInComposableStudio) ||
165+
!isEmpty(missingScanStatusAssets) ||
166+
!isEmpty(missingAssetRefsInEntries)
155167
) {
156168
if (this.currentCommand === 'cm:stacks:audit') {
157169
log.warn(this.$t(auditMsg.FINAL_REPORT_PATH, { path: this.sharedConfig.reportPath }), this.auditContext);
@@ -192,7 +204,9 @@ export abstract class AuditBaseCommand extends BaseCommand<typeof AuditBaseComma
192204
!isEmpty(missingEnvLocalesInAssets) ||
193205
!isEmpty(missingEnvLocalesInEntries) ||
194206
!isEmpty(missingFieldRules) ||
195-
!isEmpty(missingRefsInComposableStudio)
207+
!isEmpty(missingRefsInComposableStudio) ||
208+
!isEmpty(missingScanStatusAssets) ||
209+
!isEmpty(missingAssetRefsInEntries)
196210
);
197211
}
198212

@@ -223,6 +237,7 @@ export abstract class AuditBaseCommand extends BaseCommand<typeof AuditBaseComma
223237
missingTitleFields?: Record<string, any>;
224238
missingEnvLocale?: Record<string, any>;
225239
missingMultipleFields?: Record<string, any>;
240+
missingAssetRefs?: Record<string, any>;
226241
} = {},
227242
missingMandatoryFields,
228243
missingTitleFields,
@@ -231,6 +246,8 @@ export abstract class AuditBaseCommand extends BaseCommand<typeof AuditBaseComma
231246
missingEnvLocalesInEntries,
232247
missingFieldRules,
233248
missingMultipleFields,
249+
missingAssetRefsInEntries,
250+
missingScanStatusAssets,
234251
missingRefsInComposableStudio;
235252

236253
const constructorParam: ModuleConstructorParam & CtConstructorParam = {
@@ -270,17 +287,21 @@ export abstract class AuditBaseCommand extends BaseCommand<typeof AuditBaseComma
270287
constructorParam['moduleName'] = module;
271288

272289
switch (module) {
273-
case 'assets':
290+
case 'assets': {
274291
log.info('Executing assets audit', this.auditContext);
275-
const assetsTotalCount = dataModuleWise['assets']?.Total || 0;
276-
missingEnvLocalesInAssets = await new Assets(cloneDeep(constructorParam)).run(false, assetsTotalCount);
292+
const assetsModule = new Assets(cloneDeep(constructorParam));
293+
missingEnvLocalesInAssets = await assetsModule.run();
294+
missingScanStatusAssets = assetsModule.missingScanStatusAssets ?? {};
277295
await this.prepareReport(module, missingEnvLocalesInAssets);
296+
await this.prepareReport('asset-scan-status', missingScanStatusAssets);
278297
this.getAffectedData('assets', dataModuleWise['assets'], missingEnvLocalesInAssets);
298+
this.getAffectedData('asset-scan-status', dataModuleWise['assets'], missingScanStatusAssets);
279299
log.success(
280-
`Assets audit completed. Found ${Object.keys(missingEnvLocalesInAssets || {}).length} issues`,
300+
`Assets audit completed. Found ${Object.keys(missingEnvLocalesInAssets || {}).length} issues, ${Object.keys(missingScanStatusAssets).length} non-clean scan status assets`,
281301
this.auditContext,
282302
);
283303
break;
304+
}
284305
case 'content-types':
285306
log.info('Executing content-types audit', this.auditContext);
286307
const contentTypesTotalCount = dataModuleWise['content-types']?.Total || 0;
@@ -313,6 +334,7 @@ export abstract class AuditBaseCommand extends BaseCommand<typeof AuditBaseComma
313334
missingTitleFields = missingEntry.missingTitleFields ?? {};
314335
missingEnvLocalesInEntries = missingEntry.missingEnvLocale ?? {};
315336
missingMultipleFields = missingEntry.missingMultipleFields ?? {};
337+
missingAssetRefsInEntries = missingEntry.missingAssetRefs ?? {};
316338
await this.prepareReport(module, missingEntryRefs);
317339

318340
await this.prepareReport(`Entries_Select_field`, missingSelectFeild);
@@ -324,6 +346,8 @@ export abstract class AuditBaseCommand extends BaseCommand<typeof AuditBaseComma
324346
await this.prepareReport('Entry_Missing_Locale_and_Env_in_Publish_Details', missingEnvLocalesInEntries);
325347

326348
await this.prepareReport('Entry_Multiple_Fields', missingMultipleFields);
349+
350+
await this.prepareReport('Entries_Asset_field', missingAssetRefsInEntries);
327351
this.getAffectedData('entries', dataModuleWise['entries'], missingEntry);
328352
log.success(
329353
`Entries audit completed. Found ${Object.keys(missingEntryRefs || {}).length} reference issues`,
@@ -382,14 +406,14 @@ export abstract class AuditBaseCommand extends BaseCommand<typeof AuditBaseComma
382406
// schema from moduleName, so invoke it once per schema (overriding moduleName per
383407
// instance) and merge the results. Merged object is keyed by schema uid; a content type
384408
// and global field sharing a uid would collide, which is not expected in practice.
385-
const ctFieldRules = await new FieldRule(
386-
cloneDeep({ ...constructorParam, moduleName: 'content-types' }),
387-
).run(data.ctSchema?.length || 0);
409+
const ctFieldRules = await new FieldRule(cloneDeep({ ...constructorParam, moduleName: 'content-types' })).run(
410+
data.ctSchema?.length || 0,
411+
);
388412
let gfFieldRules: Record<string, any> = {};
389413
if (data.gfSchema?.length) {
390-
gfFieldRules = await new FieldRule(
391-
cloneDeep({ ...constructorParam, moduleName: 'global-fields' }),
392-
).run(data.gfSchema.length);
414+
gfFieldRules = await new FieldRule(cloneDeep({ ...constructorParam, moduleName: 'global-fields' })).run(
415+
data.gfSchema.length,
416+
);
393417
}
394418
missingFieldRules = { ...ctFieldRules, ...gfFieldRules };
395419

@@ -455,6 +479,8 @@ export abstract class AuditBaseCommand extends BaseCommand<typeof AuditBaseComma
455479
missingFieldRules,
456480
missingMultipleFields,
457481
missingRefsInComposableStudio,
482+
missingScanStatusAssets,
483+
missingAssetRefsInEntries,
458484
};
459485
}
460486

@@ -638,7 +664,8 @@ export abstract class AuditBaseCommand extends BaseCommand<typeof AuditBaseComma
638664
| keyof typeof config.moduleConfig
639665
| keyof typeof config.ReportTitleForEntries
640666
| 'field-rules'
641-
| 'Summary',
667+
| 'Summary'
668+
| 'asset-scan-status',
642669
listOfMissingRefs: Record<string, any>,
643670
): Promise<void> {
644671
log.debug(`Preparing report for module: ${moduleName}`, this.auditContext);
@@ -682,7 +709,8 @@ export abstract class AuditBaseCommand extends BaseCommand<typeof AuditBaseComma
682709
| keyof typeof config.moduleConfig
683710
| keyof typeof config.ReportTitleForEntries
684711
| 'field-rules'
685-
| 'Summary',
712+
| 'Summary'
713+
| 'asset-scan-status',
686714
listOfMissingRefs: Record<string, any>,
687715
): Promise<void> {
688716
if (Object.keys(config.moduleConfig).includes(moduleName) || config.field_level_modules.includes(moduleName)) {

‎packages/contentstack-audit/src/config/index.ts‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,9 @@ const config = {
5555
name: 'assets',
5656
dirName: 'assets',
5757
fileName: 'assets.json',
58+
// Asset scan statuses that must block import/reference; any other value (including
59+
// 'not_scanned', 'clean', or the field being absent) is treated as safe.
60+
blockingScanStatuses: ['pending', 'quarantined'],
5861
},
5962
environments: {
6063
name: 'environments',
@@ -111,6 +114,8 @@ const config = {
111114
'publish_environment',
112115
'asset_uid',
113116
'space_id',
117+
'scan_status',
118+
'mandatory',
114119
'selectedValue',
115120
'ct_uid',
116121
'action',
@@ -130,6 +135,7 @@ const config = {
130135
Entry_Missing_Locale_and_Env: 'Entry_Missing_Locale_and_Env',
131136
Entry_Missing_Locale_and_Env_in_Publish_Details: 'Entry_Missing_Locale_and_Env_in_Publish_Details',
132137
Entry_Multiple_Fields: 'Entry_Multiple_Fields',
138+
Entries_Asset_field: 'Entries_Asset_field',
133139
},
134140
field_level_modules: [
135141
'Entries_Title_field',
@@ -138,6 +144,8 @@ const config = {
138144
'Entry_Missing_Locale_and_Env_in_Publish_Details',
139145
'field-rules',
140146
'Entry_Multiple_Fields',
147+
'Entries_Asset_field',
148+
'asset-scan-status',
141149
'Summary',
142150
],
143151
fixSelectField: false,

‎packages/contentstack-audit/src/messages/index.ts‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,8 @@ const auditMsg = {
5353
FIELD_RULE_TARGET_ABSENT: `The target field '{target_field}' is not present in the schema of the content-type {ctUid}`,
5454
FIELD_RULE_CONDITION_SCAN_MESSAGE: `Completed Scanning of Field Rule '{num}' condition of Content-type '{ctUid}'`,
5555
FIELD_RULE_TARGET_SCAN_MESSAGE: `Completed Scanning of Field Rule '{num}' target of Content-type '{ctUid}'`,
56+
SCAN_ASSET_QUARANTINE_MSG: `Asset with UID '{uid}' has a non-clean scan status ('{status}') and will be excluded from import.`,
57+
ENTRY_ASSET_REF_WARN_MSG: `Entry '{uid}' field '{field}' references asset '{asset_uid}' with scan status '{status}'; it will be removed on fix.`,
5658
};
5759

5860
const auditFixMsg = {
@@ -66,7 +68,9 @@ const auditFixMsg = {
6668
ENTRY_MANDATORY_FIELD_FIX: `Removing the publish details from the entry with UID '{uid}' in Locale '{locale}'...`,
6769
ENTRY_SELECT_FIELD_FIX: `Adding the value '{value}' in the select field of entry UID '{uid}'...`,
6870
ASSET_FIX: `Fixed publish details for Asset with UID '{uid}'`,
69-
FIELD_RULE_FIX_MESSAGE: `Fixed Field Rule '{num}' target of Content-type '{ctUid}'`,
71+
FIELD_RULE_FIX_MESSAGE: `Fixed Field Rule '{num}' target of Content-type '{ctUid}`,
72+
ASSET_SCAN_STATUS_FIX: `Removed asset with UID '{uid}' (scan status: '{status}') from assets.json.`,
73+
ENTRY_ASSET_REF_FIX: `Removed reference to asset '{asset_uid}' from entry '{uid}' field '{field}'.`,
7074
};
7175

7276
const messages: typeof errors &

‎packages/contentstack-audit/src/modules/assets.ts‎

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ export default class Assets extends BaseClass {
3535
public environments: string[] = [];
3636
protected schema: ContentTypeStruct[] = [];
3737
protected missingEnvLocales: Record<string, any> = {};
38+
public missingScanStatusAssets: Record<string, any> = {};
3839
public moduleName: keyof typeof auditConfig.moduleConfig;
3940
private fixOverwriteConfirmed: boolean | null = null;
4041
/** API key of the exported stack; null when `stack/stack.json` is missing or unreadable. */
@@ -342,6 +343,30 @@ export default class Assets extends BaseClass {
342343
for (const assetUid in assets) {
343344
log.debug(`Processing asset: ${assetUid}`, this.config.auditContext);
344345

346+
const scanStatus = this.assets[assetUid]?._asset_scan_status;
347+
if (this.config.moduleConfig.assets.blockingScanStatuses.includes(scanStatus)) {
348+
log.debug(`Asset ${assetUid} has a non-clean scan status: ${scanStatus}`, this.config.auditContext);
349+
cliux.print($t(auditMsg.SCAN_ASSET_QUARANTINE_MSG, { uid: assetUid, status: scanStatus }), {
350+
color: 'yellow',
351+
});
352+
this.missingScanStatusAssets[assetUid] = [
353+
{ asset_uid: assetUid, filename: this.assets[assetUid].filename, scan_status: scanStatus, space_id: spaceId },
354+
];
355+
356+
if (this.fix) {
357+
log.info(
358+
$t(auditFixMsg.ASSET_SCAN_STATUS_FIX, { uid: assetUid, status: scanStatus }),
359+
this.config.auditContext,
360+
);
361+
delete this.assets[assetUid];
362+
chunkChanged = true;
363+
if (this.progressManager) {
364+
this.progressManager.tick(true, `asset: ${assetUid}`, null, spaceProcessName);
365+
}
366+
continue;
367+
}
368+
}
369+
345370
if (this.assets[assetUid]?.publish_details && !Array.isArray(this.assets[assetUid].publish_details)) {
346371
log.debug(`Asset ${assetUid} has invalid publish_details format`, this.config.auditContext);
347372
cliux.print($t(auditMsg.ASSET_NOT_EXIST, { uid: assetUid }), { color: 'red' });

0 commit comments

Comments
 (0)