slashdev 1.0.0 → 1.2.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/CLAUDE.md +5 -4
- package/GETTING-STARTED.md +68 -0
- package/claude-skills/salesforce-development/SKILL.md +341 -0
- package/claude-skills/salesforce-development/references/apex-patterns.md +388 -0
- package/claude-skills/salesforce-development/references/lwc-patterns.md +374 -0
- package/claude-skills/salesforce-development/references/testing-patterns.md +438 -0
- package/claude-skills/secure-deploy/SKILL.md +219 -0
- package/hooks/pre-install-guard.sh +88 -0
- package/package.json +1 -1
- package/src/commands/setup-internals.js +84 -0
- package/src/commands/setup.js +8 -2
- package/src/commands/update.js +5 -1
|
@@ -0,0 +1,438 @@
|
|
|
1
|
+
# Testing Patterns — Reference
|
|
2
|
+
|
|
3
|
+
Load this file when writing or reviewing Salesforce test classes.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## TestDataFactory
|
|
8
|
+
|
|
9
|
+
A centralized factory class that creates fully-formed test records. Every test class uses this
|
|
10
|
+
instead of building records inline — keeps tests DRY and ensures records pass validation rules.
|
|
11
|
+
|
|
12
|
+
```apex
|
|
13
|
+
@IsTest
|
|
14
|
+
public class TestDataFactory {
|
|
15
|
+
|
|
16
|
+
public static List<Account> createAccounts(Integer count, Boolean doInsert) {
|
|
17
|
+
List<Account> accounts = new List<Account>();
|
|
18
|
+
for (Integer i = 0; i < count; i++) {
|
|
19
|
+
accounts.add(new Account(
|
|
20
|
+
Name = 'Test Account ' + i,
|
|
21
|
+
Industry = 'Technology',
|
|
22
|
+
BillingStreet = '123 Test St',
|
|
23
|
+
BillingCity = 'San Francisco',
|
|
24
|
+
BillingState = 'CA',
|
|
25
|
+
BillingPostalCode = '94105',
|
|
26
|
+
BillingCountry = 'US'
|
|
27
|
+
));
|
|
28
|
+
}
|
|
29
|
+
if (doInsert) {
|
|
30
|
+
insert accounts;
|
|
31
|
+
}
|
|
32
|
+
return accounts;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
public static List<Contact> createContacts(Integer count, Id accountId, Boolean doInsert) {
|
|
36
|
+
List<Contact> contacts = new List<Contact>();
|
|
37
|
+
for (Integer i = 0; i < count; i++) {
|
|
38
|
+
contacts.add(new Contact(
|
|
39
|
+
FirstName = 'Test',
|
|
40
|
+
LastName = 'Contact ' + i,
|
|
41
|
+
Email = 'test.contact' + i + '@example.com',
|
|
42
|
+
AccountId = accountId
|
|
43
|
+
));
|
|
44
|
+
}
|
|
45
|
+
if (doInsert) {
|
|
46
|
+
insert contacts;
|
|
47
|
+
}
|
|
48
|
+
return contacts;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
public static List<Opportunity> createOpportunities(Integer count, Id accountId, Boolean doInsert) {
|
|
52
|
+
List<Opportunity> opps = new List<Opportunity>();
|
|
53
|
+
for (Integer i = 0; i < count; i++) {
|
|
54
|
+
opps.add(new Opportunity(
|
|
55
|
+
Name = 'Test Opportunity ' + i,
|
|
56
|
+
AccountId = accountId,
|
|
57
|
+
StageName = 'Prospecting',
|
|
58
|
+
CloseDate = Date.today().addDays(30),
|
|
59
|
+
Amount = 10000
|
|
60
|
+
));
|
|
61
|
+
}
|
|
62
|
+
if (doInsert) {
|
|
63
|
+
insert opps;
|
|
64
|
+
}
|
|
65
|
+
return opps;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
public static User createTestUser(String profileName, Boolean doInsert) {
|
|
69
|
+
Profile p = [SELECT Id FROM Profile WHERE Name = :profileName LIMIT 1];
|
|
70
|
+
String uniqueStr = String.valueOf(DateTime.now().getTime());
|
|
71
|
+
User u = new User(
|
|
72
|
+
FirstName = 'Test',
|
|
73
|
+
LastName = 'User ' + uniqueStr,
|
|
74
|
+
Email = 'testuser' + uniqueStr + '@example.com',
|
|
75
|
+
Username = 'testuser' + uniqueStr + '@example.com.test',
|
|
76
|
+
Alias = 'tuser',
|
|
77
|
+
TimeZoneSidKey = 'America/Los_Angeles',
|
|
78
|
+
LocaleSidKey = 'en_US',
|
|
79
|
+
EmailEncodingKey = 'UTF-8',
|
|
80
|
+
LanguageLocaleKey = 'en_US',
|
|
81
|
+
ProfileId = p.Id
|
|
82
|
+
);
|
|
83
|
+
if (doInsert) {
|
|
84
|
+
insert u;
|
|
85
|
+
}
|
|
86
|
+
return u;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
### Design principles
|
|
92
|
+
|
|
93
|
+
- **`doInsert` parameter** — lets callers get records pre-insert for field modification,
|
|
94
|
+
or inserted for immediate use. Both paths are common.
|
|
95
|
+
- **Populate required fields** — factory records must pass all validation rules and required
|
|
96
|
+
field constraints without the caller needing to know which fields are mandatory.
|
|
97
|
+
- **Use unique values** — timestamps or counters in Name/Email to avoid duplicate rule
|
|
98
|
+
violations in bulk tests.
|
|
99
|
+
|
|
100
|
+
---
|
|
101
|
+
|
|
102
|
+
## Bulk Testing
|
|
103
|
+
|
|
104
|
+
Every test should insert 200+ records to prove the code under test handles a full trigger
|
|
105
|
+
batch. A test that works with 1 record and fails with 200 has a bulkification bug.
|
|
106
|
+
|
|
107
|
+
```apex
|
|
108
|
+
@IsTest
|
|
109
|
+
private class AccountTriggerTest {
|
|
110
|
+
|
|
111
|
+
@IsTest
|
|
112
|
+
static void shouldHandleFullBatch() {
|
|
113
|
+
// 200 = one full trigger batch
|
|
114
|
+
List<Account> accounts = TestDataFactory.createAccounts(200, false);
|
|
115
|
+
|
|
116
|
+
Test.startTest();
|
|
117
|
+
insert accounts;
|
|
118
|
+
Test.stopTest();
|
|
119
|
+
|
|
120
|
+
// Assert on outcomes — query fresh to see trigger side-effects
|
|
121
|
+
List<Account> inserted = [
|
|
122
|
+
SELECT Id, Custom_Status__c
|
|
123
|
+
FROM Account
|
|
124
|
+
WHERE Id IN :accounts
|
|
125
|
+
];
|
|
126
|
+
Assert.areEqual(200, inserted.size(), 'All 200 accounts should be inserted');
|
|
127
|
+
for (Account a : inserted) {
|
|
128
|
+
Assert.areEqual('New', a.Custom_Status__c,
|
|
129
|
+
'Trigger should set default status to New');
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
### Why 200?
|
|
136
|
+
|
|
137
|
+
- Triggers fire in batches of up to 200 records (Data Loader, Bulk API, batch Apex).
|
|
138
|
+
- `Test.startTest()` resets governor limits, so the 200-record DML gets a fresh set of limits
|
|
139
|
+
— mimicking real-world trigger execution.
|
|
140
|
+
|
|
141
|
+
---
|
|
142
|
+
|
|
143
|
+
## Mocking HTTP Callouts
|
|
144
|
+
|
|
145
|
+
Salesforce blocks real HTTP callouts in tests. Implement `HttpCalloutMock` and register it
|
|
146
|
+
with `Test.setMock`.
|
|
147
|
+
|
|
148
|
+
### Single-endpoint mock
|
|
149
|
+
|
|
150
|
+
```apex
|
|
151
|
+
@IsTest
|
|
152
|
+
public class ExternalAccountServiceMock implements HttpCalloutMock {
|
|
153
|
+
|
|
154
|
+
private Integer statusCode;
|
|
155
|
+
private String body;
|
|
156
|
+
|
|
157
|
+
public ExternalAccountServiceMock(Integer statusCode, String body) {
|
|
158
|
+
this.statusCode = statusCode;
|
|
159
|
+
this.body = body;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
public HttpResponse respond(HttpRequest req) {
|
|
163
|
+
HttpResponse res = new HttpResponse();
|
|
164
|
+
res.setStatusCode(this.statusCode);
|
|
165
|
+
res.setHeader('Content-Type', 'application/json');
|
|
166
|
+
res.setBody(this.body);
|
|
167
|
+
return res;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
### Using the mock in a test
|
|
173
|
+
|
|
174
|
+
```apex
|
|
175
|
+
@IsTest
|
|
176
|
+
private class ExternalAccountServiceTest {
|
|
177
|
+
|
|
178
|
+
@IsTest
|
|
179
|
+
static void shouldDeserializeSuccessResponse() {
|
|
180
|
+
String mockBody = '{"id": "EXT-001", "name": "Acme Corp", "status": "active"}';
|
|
181
|
+
Test.setMock(HttpCalloutMock.class, new ExternalAccountServiceMock(200, mockBody));
|
|
182
|
+
|
|
183
|
+
Test.startTest();
|
|
184
|
+
ExternalAccountService.ExternalAccount result =
|
|
185
|
+
ExternalAccountService.getAccount('EXT-001');
|
|
186
|
+
Test.stopTest();
|
|
187
|
+
|
|
188
|
+
Assert.isNotNull(result, 'Result should not be null for 200 response');
|
|
189
|
+
Assert.areEqual('EXT-001', result.id, 'ID should match mock response');
|
|
190
|
+
Assert.areEqual('Acme Corp', result.name, 'Name should match mock response');
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
@IsTest
|
|
194
|
+
static void shouldReturnNullFor404() {
|
|
195
|
+
Test.setMock(HttpCalloutMock.class, new ExternalAccountServiceMock(404, ''));
|
|
196
|
+
|
|
197
|
+
Test.startTest();
|
|
198
|
+
ExternalAccountService.ExternalAccount result =
|
|
199
|
+
ExternalAccountService.getAccount('NONEXISTENT');
|
|
200
|
+
Test.stopTest();
|
|
201
|
+
|
|
202
|
+
Assert.isNull(result, 'Should return null for 404');
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
@IsTest
|
|
206
|
+
static void shouldThrowOnServerError() {
|
|
207
|
+
Test.setMock(HttpCalloutMock.class, new ExternalAccountServiceMock(500, 'Internal Server Error'));
|
|
208
|
+
|
|
209
|
+
Test.startTest();
|
|
210
|
+
try {
|
|
211
|
+
ExternalAccountService.getAccount('EXT-001');
|
|
212
|
+
Assert.fail('Should have thrown CalloutException');
|
|
213
|
+
} catch (CalloutException e) {
|
|
214
|
+
Assert.isTrue(e.getMessage().contains('500'), 'Exception should contain status code');
|
|
215
|
+
}
|
|
216
|
+
Test.stopTest();
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
### Multi-endpoint mock
|
|
222
|
+
|
|
223
|
+
For classes that call multiple endpoints, route by URL in `respond()`:
|
|
224
|
+
|
|
225
|
+
```apex
|
|
226
|
+
public HttpResponse respond(HttpRequest req) {
|
|
227
|
+
String endpoint = req.getEndpoint();
|
|
228
|
+
HttpResponse res = new HttpResponse();
|
|
229
|
+
res.setHeader('Content-Type', 'application/json');
|
|
230
|
+
|
|
231
|
+
if (endpoint.contains('/accounts')) {
|
|
232
|
+
res.setStatusCode(200);
|
|
233
|
+
res.setBody('{"id": "ACC-001"}');
|
|
234
|
+
} else if (endpoint.contains('/contacts')) {
|
|
235
|
+
res.setStatusCode(200);
|
|
236
|
+
res.setBody('{"id": "CON-001"}');
|
|
237
|
+
} else {
|
|
238
|
+
res.setStatusCode(404);
|
|
239
|
+
res.setBody('Not found');
|
|
240
|
+
}
|
|
241
|
+
return res;
|
|
242
|
+
}
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
---
|
|
246
|
+
|
|
247
|
+
## Async Testing — Test.startTest() / Test.stopTest()
|
|
248
|
+
|
|
249
|
+
`Test.startTest()` and `Test.stopTest()` do two things:
|
|
250
|
+
|
|
251
|
+
1. **Reset governor limits** — code between them gets a fresh set of limits, isolating the
|
|
252
|
+
code under test from test setup overhead.
|
|
253
|
+
2. **Force async execution** — `@future`, `Queueable`, and `Batch Apex` jobs enqueued between
|
|
254
|
+
`startTest`/`stopTest` execute synchronously at `stopTest()`, making them testable.
|
|
255
|
+
|
|
256
|
+
### Testing a Queueable
|
|
257
|
+
|
|
258
|
+
```apex
|
|
259
|
+
@IsTest
|
|
260
|
+
private class SyncAccountToERPTest {
|
|
261
|
+
|
|
262
|
+
@IsTest
|
|
263
|
+
static void shouldCallERPEndpoint() {
|
|
264
|
+
Account a = TestDataFactory.createAccounts(1, true)[0];
|
|
265
|
+
Test.setMock(HttpCalloutMock.class, new ExternalAccountServiceMock(200, '{"status":"ok"}'));
|
|
266
|
+
|
|
267
|
+
Test.startTest();
|
|
268
|
+
System.enqueueJob(new SyncAccountToERP(new List<Id>{ a.Id }));
|
|
269
|
+
Test.stopTest(); // Queueable executes here synchronously
|
|
270
|
+
|
|
271
|
+
// Assert on side effects — e.g., a log record, field update, etc.
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
### Testing Batch Apex
|
|
277
|
+
|
|
278
|
+
```apex
|
|
279
|
+
@IsTest
|
|
280
|
+
private class DeactivateStaleLeadsBatchTest {
|
|
281
|
+
|
|
282
|
+
@IsTest
|
|
283
|
+
static void shouldDeactivateStaleLeads() {
|
|
284
|
+
List<Lead> leads = new List<Lead>();
|
|
285
|
+
for (Integer i = 0; i < 200; i++) {
|
|
286
|
+
leads.add(new Lead(
|
|
287
|
+
FirstName = 'Test', LastName = 'Lead ' + i, Company = 'TestCo',
|
|
288
|
+
Status = 'Open'
|
|
289
|
+
));
|
|
290
|
+
}
|
|
291
|
+
insert leads;
|
|
292
|
+
// Backdate LastActivityDate via Test.setCreatedDate or a field update
|
|
293
|
+
// (LastActivityDate is read-only — use a proxy field or adjust the query for testing)
|
|
294
|
+
|
|
295
|
+
Test.startTest();
|
|
296
|
+
Database.executeBatch(new DeactivateStaleLeadsBatch(), 200);
|
|
297
|
+
Test.stopTest(); // All batch execute() calls run here
|
|
298
|
+
|
|
299
|
+
List<Lead> updated = [SELECT Status FROM Lead WHERE Id IN :leads];
|
|
300
|
+
for (Lead l : updated) {
|
|
301
|
+
Assert.areEqual('Closed - Not Converted', l.Status, 'Stale leads should be deactivated');
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
---
|
|
308
|
+
|
|
309
|
+
## System.runAs — Testing Sharing and FLS
|
|
310
|
+
|
|
311
|
+
`System.runAs` executes code in the context of a specific user, enforcing their profile
|
|
312
|
+
permissions, sharing rules, and field-level security. Use it to verify that `with sharing`
|
|
313
|
+
classes and FLS checks work correctly.
|
|
314
|
+
|
|
315
|
+
```apex
|
|
316
|
+
@IsTest
|
|
317
|
+
private class AccountServiceTest {
|
|
318
|
+
|
|
319
|
+
@IsTest
|
|
320
|
+
static void shouldEnforceSharingRules() {
|
|
321
|
+
// Create a restricted user
|
|
322
|
+
User restrictedUser = TestDataFactory.createTestUser('Standard User', true);
|
|
323
|
+
|
|
324
|
+
// Create an account owned by the running user (not the restricted user)
|
|
325
|
+
Account privateAccount = TestDataFactory.createAccounts(1, true)[0];
|
|
326
|
+
|
|
327
|
+
System.runAs(restrictedUser) {
|
|
328
|
+
Test.startTest();
|
|
329
|
+
List<Account> visible = new AccountService().getVisibleAccounts();
|
|
330
|
+
Test.stopTest();
|
|
331
|
+
|
|
332
|
+
// Restricted user should NOT see the private account
|
|
333
|
+
// (assuming OWD for Account is Private)
|
|
334
|
+
Set<Id> visibleIds = new Set<Id>();
|
|
335
|
+
for (Account a : visible) {
|
|
336
|
+
visibleIds.add(a.Id);
|
|
337
|
+
}
|
|
338
|
+
Assert.isFalse(visibleIds.contains(privateAccount.Id),
|
|
339
|
+
'Restricted user should not see accounts they do not own');
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
```
|
|
344
|
+
|
|
345
|
+
### Caveats
|
|
346
|
+
|
|
347
|
+
- `System.runAs` does NOT count against governor limits for the enclosing test.
|
|
348
|
+
- Mixed DML restrictions apply — inserting a User and a standard object in the same
|
|
349
|
+
transaction throws an error. Insert the user first, then use `System.runAs` for the rest.
|
|
350
|
+
- `System.runAs` only works in `@IsTest` methods.
|
|
351
|
+
|
|
352
|
+
---
|
|
353
|
+
|
|
354
|
+
## @testSetup — Shared Test Data
|
|
355
|
+
|
|
356
|
+
`@testSetup` runs once per test class and creates records available to all `@IsTest` methods
|
|
357
|
+
in that class. Each test method gets its own copy (rolled back after each method), so tests
|
|
358
|
+
remain independent.
|
|
359
|
+
|
|
360
|
+
```apex
|
|
361
|
+
@IsTest
|
|
362
|
+
private class OpportunityServiceTest {
|
|
363
|
+
|
|
364
|
+
@testSetup
|
|
365
|
+
static void setupData() {
|
|
366
|
+
List<Account> accounts = TestDataFactory.createAccounts(5, true);
|
|
367
|
+
for (Account a : accounts) {
|
|
368
|
+
TestDataFactory.createOpportunities(10, a.Id, true);
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
@IsTest
|
|
373
|
+
static void shouldCalculateTotalPipeline() {
|
|
374
|
+
Test.startTest();
|
|
375
|
+
Decimal total = OpportunityService.getTotalPipeline();
|
|
376
|
+
Test.stopTest();
|
|
377
|
+
|
|
378
|
+
Assert.areEqual(500000, total, '5 accounts x 10 opps x $10,000 = $500,000');
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
@IsTest
|
|
382
|
+
static void shouldFilterByStage() {
|
|
383
|
+
// This test gets the SAME data as above — fresh copy, not affected by other tests
|
|
384
|
+
List<Opportunity> opps = [SELECT Id FROM Opportunity WHERE StageName = 'Prospecting'];
|
|
385
|
+
Assert.areEqual(50, opps.size(), 'All 50 opps should be in Prospecting stage');
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
```
|
|
389
|
+
|
|
390
|
+
### Caveats
|
|
391
|
+
|
|
392
|
+
- `@testSetup` cannot be used with `@IsTest(SeeAllData=true)` — they are mutually exclusive.
|
|
393
|
+
- Records created in `@testSetup` are not visible to the `@testSetup` method's own
|
|
394
|
+
`System.runAs` blocks — each `@IsTest` method sees a fresh snapshot.
|
|
395
|
+
- If any `@IsTest` method needs different data, create it within that method — don't
|
|
396
|
+
over-centralize.
|
|
397
|
+
|
|
398
|
+
---
|
|
399
|
+
|
|
400
|
+
## Assert Patterns
|
|
401
|
+
|
|
402
|
+
### Modern Assert class (Spring '22+)
|
|
403
|
+
|
|
404
|
+
Prefer `Assert` over the legacy `System.assert*` methods. The modern class has clearer
|
|
405
|
+
method names and better error messages.
|
|
406
|
+
|
|
407
|
+
```apex
|
|
408
|
+
// Equality
|
|
409
|
+
Assert.areEqual(expected, actual, 'Descriptive message explaining what went wrong');
|
|
410
|
+
Assert.areNotEqual(unexpected, actual, 'Values should differ because...');
|
|
411
|
+
|
|
412
|
+
// Null checks
|
|
413
|
+
Assert.isNull(value, 'Should be null because...');
|
|
414
|
+
Assert.isNotNull(value, 'Should not be null because...');
|
|
415
|
+
|
|
416
|
+
// Boolean
|
|
417
|
+
Assert.isTrue(condition, 'Condition should be true because...');
|
|
418
|
+
Assert.isFalse(condition, 'Condition should be false because...');
|
|
419
|
+
|
|
420
|
+
// Forced failure (in catch blocks for expected exceptions)
|
|
421
|
+
Assert.fail('Should have thrown an exception');
|
|
422
|
+
|
|
423
|
+
// Instance check
|
|
424
|
+
Assert.isInstanceOfType(obj, Account.class, 'Should be an Account');
|
|
425
|
+
```
|
|
426
|
+
|
|
427
|
+
### Rules
|
|
428
|
+
|
|
429
|
+
- **Always include a message parameter.** When a test fails in a CI pipeline, the message
|
|
430
|
+
is the only context the developer has. "Expected 200 but got 0" is useless without
|
|
431
|
+
"All accounts should have been inserted by the bulk trigger."
|
|
432
|
+
- **Assert on specific values, not just non-null.** `Assert.isNotNull(result)` proves the
|
|
433
|
+
query returned something — it doesn't prove it returned the *right* thing.
|
|
434
|
+
- **One logical assertion per test method** when practical. Multiple assertions are fine if
|
|
435
|
+
they verify the same operation's outcome (e.g., checking multiple fields on a record), but
|
|
436
|
+
avoid combining unrelated assertions that make failures ambiguous.
|
|
437
|
+
- **Never use `System.assertEquals` in new code** — it still works but `Assert.areEqual` is
|
|
438
|
+
the modern replacement with consistent naming.
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: secure-deploy
|
|
3
|
+
description: Retrofit a project's CI/CD pipelines, Dockerfiles, and deploy scripts to use Socket Firewall (sfw) for all dependency installs. Use when the user asks to "secure deploy", "set up sfw in CI", "add socket firewall to production", or whenever you're editing a CI workflow / Dockerfile / deploy script that installs dependencies.
|
|
4
|
+
version: "1.0"
|
|
5
|
+
owner: michael@slashdev.io
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Secure Deploy — Retrofit projects to use Socket Firewall in production
|
|
9
|
+
|
|
10
|
+
Slashdev requires every dependency install — local AND production — to be wrapped
|
|
11
|
+
with Socket Firewall (`sfw`). This protects against supply-chain attacks
|
|
12
|
+
(compromised packages, typosquats, dependency confusion) by scanning every package
|
|
13
|
+
against Socket's vulnerability/malware database before it's allowed to install.
|
|
14
|
+
|
|
15
|
+
When a developer asks you to set up production deployment, edit a CI pipeline, write
|
|
16
|
+
a Dockerfile, or deploy a project, **you must ensure every install command uses `sfw`**.
|
|
17
|
+
|
|
18
|
+
This skill walks you through the retrofit systematically.
|
|
19
|
+
|
|
20
|
+
## When to use this skill
|
|
21
|
+
|
|
22
|
+
Trigger automatically (without being asked) whenever you're about to:
|
|
23
|
+
- Create or edit a `.github/workflows/*.yml` file with an `npm`/`pnpm`/`yarn`/`bun`/`composer`/`pip` install step
|
|
24
|
+
- Create or edit a `Dockerfile` that runs an install command
|
|
25
|
+
- Create or edit `.gitlab-ci.yml`, `bitbucket-pipelines.yml`, `circleci/config.yml`, or any other CI config
|
|
26
|
+
- Create or edit a `deploy.sh`, `Procfile`, or `Makefile` install target
|
|
27
|
+
- Modify the `scripts` section of `package.json` to add an install-related step
|
|
28
|
+
|
|
29
|
+
Trigger explicitly when the user says: "secure deploy", "add sfw to CI", "set up socket firewall in production", "retrofit production for socket firewall", "audit deploy for sfw".
|
|
30
|
+
|
|
31
|
+
## Step-by-step retrofit playbook
|
|
32
|
+
|
|
33
|
+
### Step 1 — Discover deployment surface
|
|
34
|
+
|
|
35
|
+
Use Glob to find every file that could install dependencies in CI or production:
|
|
36
|
+
|
|
37
|
+
```
|
|
38
|
+
.github/workflows/*.yml
|
|
39
|
+
.github/workflows/*.yaml
|
|
40
|
+
.gitlab-ci.yml
|
|
41
|
+
.gitlab/*.yml
|
|
42
|
+
bitbucket-pipelines.yml
|
|
43
|
+
.circleci/config.yml
|
|
44
|
+
azure-pipelines.yml
|
|
45
|
+
Jenkinsfile
|
|
46
|
+
Dockerfile
|
|
47
|
+
Dockerfile.*
|
|
48
|
+
*.dockerfile
|
|
49
|
+
docker-compose*.yml
|
|
50
|
+
Procfile
|
|
51
|
+
deploy.sh
|
|
52
|
+
deploy/*.sh
|
|
53
|
+
scripts/deploy*.sh
|
|
54
|
+
Makefile
|
|
55
|
+
package.json
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
For each file found, Read it and look for any of these patterns:
|
|
59
|
+
|
|
60
|
+
- `npm install`, `npm i`, `npm ci`, `npm add`
|
|
61
|
+
- `pnpm install`, `pnpm i`, `pnpm add`
|
|
62
|
+
- `yarn`, `yarn install`, `yarn add`
|
|
63
|
+
- `bun install`, `bun i`, `bun add`
|
|
64
|
+
- `composer install`, `composer require`, `composer update`
|
|
65
|
+
- `pip install`, `pip3 install`, `uv install`, `uv sync`, `uv add`
|
|
66
|
+
|
|
67
|
+
Note any line that already starts with `sfw ` — those are already secured and don't
|
|
68
|
+
need to be touched.
|
|
69
|
+
|
|
70
|
+
### Step 2 — Plan the changes
|
|
71
|
+
|
|
72
|
+
For each install command found, plan one of these rewrites:
|
|
73
|
+
|
|
74
|
+
**a) GitHub Actions workflow YAML**
|
|
75
|
+
|
|
76
|
+
Before:
|
|
77
|
+
```yaml
|
|
78
|
+
- name: Install dependencies
|
|
79
|
+
run: npm ci
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
After:
|
|
83
|
+
```yaml
|
|
84
|
+
- name: Install Socket Firewall
|
|
85
|
+
run: npm install -g @socketsecurity/cli
|
|
86
|
+
- name: Install dependencies
|
|
87
|
+
run: sfw npm ci
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
If a job has multiple install steps, only one `Install Socket Firewall` step is needed
|
|
91
|
+
per job (it persists across steps because they share the same runner).
|
|
92
|
+
|
|
93
|
+
**b) Dockerfile**
|
|
94
|
+
|
|
95
|
+
Before:
|
|
96
|
+
```dockerfile
|
|
97
|
+
RUN npm ci
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
After:
|
|
101
|
+
```dockerfile
|
|
102
|
+
RUN npm install -g @socketsecurity/cli && sfw npm ci
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
For Composer:
|
|
106
|
+
```dockerfile
|
|
107
|
+
RUN npm install -g @socketsecurity/cli && sfw composer install --no-dev --optimize-autoloader
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
(npm is the simplest install vector for sfw itself; if the base image doesn't have
|
|
111
|
+
Node, install Node first or use the official Socket installer.)
|
|
112
|
+
|
|
113
|
+
**c) Shell deploy scripts (`deploy.sh`, etc.)**
|
|
114
|
+
|
|
115
|
+
Before:
|
|
116
|
+
```bash
|
|
117
|
+
npm ci --omit=dev
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
After:
|
|
121
|
+
```bash
|
|
122
|
+
# Ensure sfw is available
|
|
123
|
+
if ! command -v sfw >/dev/null 2>&1; then
|
|
124
|
+
npm install -g @socketsecurity/cli
|
|
125
|
+
fi
|
|
126
|
+
sfw npm ci --omit=dev
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
**d) `package.json` scripts**
|
|
130
|
+
|
|
131
|
+
Before:
|
|
132
|
+
```json
|
|
133
|
+
{
|
|
134
|
+
"scripts": {
|
|
135
|
+
"postinstall": "npm install --prefix ./client"
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
After:
|
|
141
|
+
```json
|
|
142
|
+
{
|
|
143
|
+
"scripts": {
|
|
144
|
+
"postinstall": "sfw npm install --prefix ./client"
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
(Don't add the `npm install -g @socketsecurity/cli` step inside `package.json` — assume
|
|
150
|
+
sfw is already installed wherever this script runs. If it's running in CI, the workflow
|
|
151
|
+
or Dockerfile should have installed sfw at the top.)
|
|
152
|
+
|
|
153
|
+
### Step 3 — Show the diff and confirm
|
|
154
|
+
|
|
155
|
+
Before writing any changes, show the user a clear summary:
|
|
156
|
+
|
|
157
|
+
> Found N install commands across M files. Proposed changes:
|
|
158
|
+
>
|
|
159
|
+
> 1. `.github/workflows/ci.yml` — wrap `npm ci` in build job (+ add sfw install step)
|
|
160
|
+
> 2. `Dockerfile` — wrap `RUN npm ci` with sfw install
|
|
161
|
+
> 3. `package.json` — wrap postinstall script
|
|
162
|
+
>
|
|
163
|
+
> Apply these changes?
|
|
164
|
+
|
|
165
|
+
Use AskUserQuestion if there's any ambiguity (e.g. multiple jobs in a workflow, unclear
|
|
166
|
+
whether a step is part of the build or local-only). For Dockerfiles especially, ask
|
|
167
|
+
about the base image — if it doesn't have Node, the sfw install command needs to be
|
|
168
|
+
different.
|
|
169
|
+
|
|
170
|
+
### Step 4 — Apply the changes
|
|
171
|
+
|
|
172
|
+
Use Edit to apply each change. After applying:
|
|
173
|
+
|
|
174
|
+
1. **Validate YAML/JSON syntax** — for any modified `.yml` or `.json` file, run a quick
|
|
175
|
+
parse check (e.g. `python3 -c 'import yaml,sys; yaml.safe_load(open("path"))'` for
|
|
176
|
+
YAML, or `python3 -m json.tool path` for JSON). Fix any syntax errors.
|
|
177
|
+
2. **Run a build smoke test if possible** — for Dockerfiles, suggest the user run
|
|
178
|
+
`docker build .` locally. For GitHub Actions, suggest pushing to a feature branch
|
|
179
|
+
and watching the workflow run.
|
|
180
|
+
3. **Document what was changed** — print a final summary listing every file modified
|
|
181
|
+
and what was added.
|
|
182
|
+
|
|
183
|
+
### Step 5 — Verify CI doesn't already use a different security tool
|
|
184
|
+
|
|
185
|
+
If during Step 1 you find references to other security scanners (Snyk, Dependabot,
|
|
186
|
+
Trivy, npm audit, etc.), don't remove them — they complement Socket Firewall, they
|
|
187
|
+
don't replace it. Mention to the user: "I found existing security scanning via X. I
|
|
188
|
+
left it in place — Socket Firewall complements it by blocking malicious packages
|
|
189
|
+
*before* they install, while X scans the installed tree."
|
|
190
|
+
|
|
191
|
+
## Important conventions
|
|
192
|
+
|
|
193
|
+
- **Never silently change install commands.** Always show the diff and get
|
|
194
|
+
confirmation before writing CI/deploy files. These are production-critical files.
|
|
195
|
+
- **Don't add sfw to dev-only `node_modules` install steps in `package.json`** unless
|
|
196
|
+
the user explicitly asks. The PreToolUse hook handles dev-side enforcement
|
|
197
|
+
automatically; only retrofit `package.json` scripts that run in CI/deploy contexts.
|
|
198
|
+
- **Don't try to install Socket Firewall globally on the user's machine** as part of
|
|
199
|
+
this skill. That's the developer's responsibility, and the slashdev hook will warn
|
|
200
|
+
them if it's missing. This skill only modifies project files.
|
|
201
|
+
- **Don't touch `.lock` files** (`package-lock.json`, `pnpm-lock.yaml`, `yarn.lock`,
|
|
202
|
+
`composer.lock`, `bun.lockb`). They're generated.
|
|
203
|
+
- **Preserve existing flags** — when wrapping `npm ci --omit=dev` with `sfw`, keep
|
|
204
|
+
the `--omit=dev` flag.
|
|
205
|
+
|
|
206
|
+
## Output format
|
|
207
|
+
|
|
208
|
+
Always end the skill with:
|
|
209
|
+
|
|
210
|
+
```
|
|
211
|
+
✓ Secured N install commands across M files:
|
|
212
|
+
- <file>: <change summary>
|
|
213
|
+
- <file>: <change summary>
|
|
214
|
+
|
|
215
|
+
Next steps:
|
|
216
|
+
- Test locally (if applicable)
|
|
217
|
+
- Push to a feature branch and watch CI
|
|
218
|
+
- Confirm sfw is available in your CI environment (the install step is now in place)
|
|
219
|
+
```
|