@guardian/pan-domain-node 0.5.1 → 1.1.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/src/panda.ts CHANGED
@@ -1,8 +1,11 @@
1
1
  import * as cookie from 'cookie';
2
2
 
3
3
  import {parseCookie, parseUser, sign, verifySignature} from './utils';
4
- import {AuthenticationStatus, User, AuthenticationResult, ValidateUserFn} from './api';
4
+ import {User, AuthenticationResult, ValidateUserFn, gracePeriodInMillis} from './api';
5
5
  import { fetchPublicKey, PublicKeyHolder } from './fetch-public-key';
6
+ import { S3 } from "@aws-sdk/client-s3";
7
+ import { fromNodeProviderChain } from "@aws-sdk/credential-providers";
8
+ import { AwsCredentialIdentityProvider } from "@aws-sdk/types";
6
9
 
7
10
  export function createCookie(user: User, privateKey: string): string {
8
11
  let queryParams: string[] = [];
@@ -25,34 +28,71 @@ export function createCookie(user: User, privateKey: string): string {
25
28
  }
26
29
 
27
30
  export function verifyUser(pandaCookie: string | undefined, publicKey: string, currentTime: Date, validateUser: ValidateUserFn): AuthenticationResult {
28
- if(!pandaCookie) {
29
- return { status: AuthenticationStatus.INVALID_COOKIE };
31
+ if (!pandaCookie) {
32
+ return {
33
+ success: false,
34
+ reason: 'no-cookie'
35
+ };
30
36
  }
31
37
 
32
- const { data, signature } = parseCookie(pandaCookie);
38
+ const parsedCookie = parseCookie(pandaCookie);
39
+ if (!parsedCookie) {
40
+ return {
41
+ success: false,
42
+ reason: 'invalid-cookie'
43
+ };
44
+ }
45
+ const { data, signature } = parsedCookie;
33
46
 
34
- if(!verifySignature(data, signature, publicKey)) {
35
- return { status: AuthenticationStatus.INVALID_COOKIE };
47
+ if (!verifySignature(data, signature, publicKey)) {
48
+ return {
49
+ success: false,
50
+ reason: 'invalid-cookie'
51
+ };
36
52
  }
37
53
 
38
- const currentTimestampInMilliseconds = currentTime.getTime();
54
+ const currentTimestampInMillis = currentTime.getTime();
39
55
 
40
56
  try {
41
57
  const user: User = parseUser(data);
42
- const isExpired = user.expires < currentTimestampInMilliseconds;
43
-
44
- if(isExpired) {
45
- return { status: AuthenticationStatus.EXPIRED, user };
58
+ const isExpired = user.expires < currentTimestampInMillis;
59
+
60
+ if (isExpired) {
61
+ const gracePeriodEndsAtEpochTimeMillis = user.expires + gracePeriodInMillis;
62
+ if (gracePeriodEndsAtEpochTimeMillis < currentTimestampInMillis) {
63
+ return {
64
+ success: false,
65
+ reason: 'expired-cookie'
66
+ };
67
+ } else {
68
+ return {
69
+ success: true,
70
+ shouldRefreshCredentials: true,
71
+ mustRefreshByEpochTimeMillis: gracePeriodEndsAtEpochTimeMillis,
72
+ user
73
+ }
74
+ }
46
75
  }
47
76
 
48
- if(!validateUser(user)) {
49
- return { status: AuthenticationStatus.NOT_AUTHORISED, user };
77
+ if (!validateUser(user)) {
78
+ return {
79
+ success: false,
80
+ reason: 'invalid-user',
81
+ user
82
+ };
50
83
  }
51
84
 
52
- return { status: AuthenticationStatus.AUTHORISED, user };
53
- } catch(error) {
85
+ return {
86
+ success: true,
87
+ shouldRefreshCredentials: false,
88
+ user
89
+ };
90
+ } catch (error) {
54
91
  console.error(error);
55
- return { status: AuthenticationStatus.INVALID_COOKIE };
92
+ return {
93
+ success: false,
94
+ reason: 'unknown'
95
+ };
56
96
  }
57
97
  }
58
98
 
@@ -64,19 +104,25 @@ export class PanDomainAuthentication {
64
104
  validateUser: ValidateUserFn;
65
105
 
66
106
  publicKey: Promise<PublicKeyHolder>;
67
- keyCacheTime: number = 60 * 1000; // 1 minute
107
+ keyCacheTimeInMillis: number = 60 * 1000; // 1 minute
68
108
  keyUpdateTimer?: NodeJS.Timeout;
109
+ s3Client: S3;
69
110
 
70
- constructor(cookieName: string, region: string, bucket: string, keyFile: string, validateUser: ValidateUserFn) {
111
+ constructor(cookieName: string, region: string, bucket: string, keyFile: string, validateUser: ValidateUserFn, credentialsProvider: AwsCredentialIdentityProvider = fromNodeProviderChain()) {
71
112
  this.cookieName = cookieName;
72
113
  this.region = region;
73
114
  this.bucket = bucket;
74
115
  this.keyFile = keyFile;
75
116
  this.validateUser = validateUser;
76
117
 
77
- this.publicKey = fetchPublicKey(region, bucket, keyFile);
118
+ const standardAwsConfig = {
119
+ region: region,
120
+ credentials: credentialsProvider,
121
+ };
122
+ this.s3Client = new S3(standardAwsConfig);
123
+ this.publicKey = fetchPublicKey(this.s3Client, bucket, keyFile);
78
124
 
79
- this.keyUpdateTimer = setInterval(() => this.getPublicKey(), this.keyCacheTime);
125
+ this.keyUpdateTimer = setInterval(() => this.getPublicKey(), this.keyCacheTimeInMillis);
80
126
  }
81
127
 
82
128
  stop(): void {
@@ -91,8 +137,8 @@ export class PanDomainAuthentication {
91
137
  const now = new Date();
92
138
  const diff = now.getTime() - lastUpdated.getTime();
93
139
 
94
- if(diff > this.keyCacheTime) {
95
- this.publicKey = fetchPublicKey(this.region, this.bucket, this.keyFile);
140
+ if(diff > this.keyCacheTimeInMillis) {
141
+ this.publicKey = fetchPublicKey(this.s3Client, this.bucket, this.keyFile);
96
142
  return this.publicKey.then(({ key }) => key);
97
143
  } else {
98
144
  return key;
@@ -104,7 +150,6 @@ export class PanDomainAuthentication {
104
150
  return this.getPublicKey().then(publicKey => {
105
151
  const cookies = cookie.parse(requestCookies);
106
152
  const pandaCookie = cookies[this.cookieName];
107
-
108
153
  return verifyUser(pandaCookie, publicKey, new Date(), this.validateUser);
109
154
  });
110
155
  }
package/src/utils.ts CHANGED
@@ -8,14 +8,40 @@ export function decodeBase64(data: string): string {
8
8
  return Buffer.from(data, 'base64').toString('utf8');
9
9
  }
10
10
 
11
+ export type ParsedCookie = { data: string, signature: string };
12
+
13
+ /**
14
+ * Check if a string is valid base64
15
+ */
16
+ function isBase64(str: string): boolean {
17
+ try {
18
+ return Buffer.from(str, 'base64').toString('base64') === str;
19
+ } catch (err) {
20
+ return false;
21
+ }
22
+ }
23
+
11
24
  /**
12
25
  * Parse a pan-domain user cookie in to data and signature
26
+ * Validates that the cookie is properly formatted (two base64 strings separated by '.')
13
27
  */
14
- export function parseCookie(cookie: string): { data: string, signature: string} {
15
- const splitCookie = cookie.split('\.');
28
+ export function parseCookie(cookie: string): ParsedCookie | undefined {
29
+ const cookieRegex = /^([\w\W]*)\.([\w\W]*)$/;
30
+ const match = cookie.match(cookieRegex);
31
+
32
+ if (!match) {
33
+ return undefined;
34
+ }
35
+
36
+ const [, data, signature] = match;
37
+
38
+ if (!isBase64(data) || !isBase64(signature)) {
39
+ return undefined;
40
+ }
41
+
16
42
  return {
17
- data: decodeBase64(splitCookie[0]),
18
- signature: splitCookie[1]
43
+ data: decodeBase64(data),
44
+ signature: signature
19
45
  };
20
46
  }
21
47
 
@@ -1,4 +1,9 @@
1
- import {guardianValidation, AuthenticationStatus, User} from '../src/api';
1
+ import {
2
+ CookieFailure, FreshSuccess,
3
+ gracePeriodInMillis,
4
+ guardianValidation, StaleSuccess,
5
+ User, UserValidationFailure
6
+ } from '../src/api';
2
7
  import { verifyUser, createCookie, PanDomainAuthentication } from '../src/panda';
3
8
  import { fetchPublicKey } from '../src/fetch-public-key';
4
9
 
@@ -9,37 +14,132 @@ import {
9
14
  publicKey,
10
15
  privateKey
11
16
  } from './fixtures';
12
- import {decodeBase64} from "../src/utils";
17
+ import {decodeBase64, parseCookie, ParsedCookie, parseUser} from "../src/utils";
13
18
 
14
19
  jest.mock('../src/fetch-public-key');
15
- jest.useFakeTimers('modern');
20
+ jest.useFakeTimers();
21
+
22
+ function userFromCookie(cookie: string): User {
23
+ // This function is only used to generate a `User` object from
24
+ // a well-formed text fixture cookie, in order to check that successful
25
+ // `AuthenticationResult`s have the right shape. As such we don't want
26
+ // to have to deal with the case of a bad cookie so we just cast to `ParsedCookie`.
27
+ const parsedCookie = parseCookie(cookie) as ParsedCookie;
28
+ return parseUser(parsedCookie.data);
29
+ }
16
30
 
17
31
  describe('verifyUser', function () {
18
32
 
19
- test("return invalid cookie if missing", () => {
20
- expect(verifyUser(undefined, "", new Date(0), guardianValidation).status).toBe(AuthenticationStatus.INVALID_COOKIE);
33
+ test("fail to authenticate if cookie is missing", () => {
34
+ const expected: CookieFailure = {
35
+ success: false,
36
+ reason: 'no-cookie'
37
+ };
38
+ expect(verifyUser(undefined, "", new Date(0), guardianValidation)).toStrictEqual(expected);
21
39
  });
22
40
 
23
- test("return invalid cookie for a malformed signature", () => {
41
+ test("fail to authenticate if signature is malformed", () => {
24
42
  const [data, signature] = sampleCookie.split(".");
25
43
  const testCookie = data + ".1234";
26
44
 
27
- expect(verifyUser(testCookie, publicKey, new Date(0), guardianValidation).status).toBe(AuthenticationStatus.INVALID_COOKIE);
45
+ const expected: CookieFailure = {
46
+ success: false,
47
+ reason: 'invalid-cookie'
48
+ };
49
+ expect(verifyUser(testCookie, publicKey, new Date(0), guardianValidation)).toStrictEqual(expected);
50
+ });
51
+
52
+ test("fail to authenticate if cookie expired and we're outside the grace period", () => {
53
+ // Cookie expires at epoch time 1234
54
+ const afterEndOfGracePeriod = new Date(1234 + gracePeriodInMillis + 1)
55
+ const expected: CookieFailure = {
56
+ success: false,
57
+ reason: 'expired-cookie'
58
+ };
59
+ expect(verifyUser(sampleCookie, publicKey, afterEndOfGracePeriod, guardianValidation)).toStrictEqual(expected);
60
+ });
61
+
62
+ test("fail to authenticate if user fails validation function", () => {
63
+ expect(verifyUser(sampleCookieWithoutMultifactor, publicKey, new Date(0), guardianValidation)).toStrictEqual({
64
+ success: false,
65
+ reason: 'invalid-user',
66
+ user: userFromCookie(sampleCookieWithoutMultifactor)
67
+ });
68
+ expect(verifyUser(sampleNonGuardianCookie, publicKey, new Date(0), guardianValidation)).toStrictEqual({
69
+ success: false,
70
+ reason: 'invalid-user',
71
+ user: userFromCookie(sampleNonGuardianCookie)
72
+ });
73
+ });
74
+
75
+ test("fail to authenticate with invalid-cookie reason if signature is not valid", () => {
76
+ const expected: CookieFailure = {
77
+ success: false,
78
+ reason: 'invalid-cookie'
79
+ };
80
+ const slightlyBadCookie = sampleCookie.slice(0, -2);
81
+ expect(verifyUser(slightlyBadCookie, publicKey, new Date(0), guardianValidation)).toStrictEqual(expected);
82
+ });
83
+
84
+ test("fail to authenticate with invalid-cookie reason if data part is not base64", () => {
85
+ const expected: CookieFailure = {
86
+ success: false,
87
+ reason: 'invalid-cookie'
88
+ };
89
+ const [_, signature] = sampleCookie.split(".");
90
+ const nonBase64Data = "not-base64-data";
91
+ const testCookie = `${nonBase64Data}.${signature}`;
92
+ expect(verifyUser(testCookie, publicKey, new Date(0), guardianValidation)).toStrictEqual(expected);
28
93
  });
29
94
 
30
- test("return expired", () => {
31
- const someTimeInTheFuture = new Date(5678);
32
- expect(someTimeInTheFuture.getTime()).toBe(5678);
33
- expect(verifyUser(sampleCookie, publicKey, someTimeInTheFuture, guardianValidation).status).toBe(AuthenticationStatus.EXPIRED);
95
+ test("fail to authenticate with invalid-cookie reason if signature part is not base64", () => {
96
+ const expected: CookieFailure = {
97
+ success: false,
98
+ reason: 'invalid-cookie'
99
+ };
100
+ const [data, _] = sampleCookie.split(".");
101
+ const nonBase64Signature = "not-base64-signature";
102
+ const testCookie = `${data}.${nonBase64Signature}`;
103
+ expect(verifyUser(testCookie, publicKey, new Date(0), guardianValidation)).toStrictEqual(expected);
34
104
  });
35
105
 
36
- test("return not authenticated if user fails validation function", () => {
37
- expect(verifyUser(sampleCookieWithoutMultifactor, publicKey, new Date(0), guardianValidation).status).toBe(AuthenticationStatus.NOT_AUTHORISED);
38
- expect(verifyUser(sampleNonGuardianCookie, publicKey, new Date(0), guardianValidation).status).toBe(AuthenticationStatus.NOT_AUTHORISED);
106
+ test("fail to authenticate with invalid-cookie reason if cookie has no dot separator", () => {
107
+ const expected: CookieFailure = {
108
+ success: false,
109
+ reason: 'invalid-cookie'
110
+ };
111
+ const noDotCookie = sampleCookie.replace(".", "");
112
+ expect(verifyUser(noDotCookie, publicKey, new Date(0), guardianValidation)).toStrictEqual(expected);
39
113
  });
40
114
 
41
- test("return authenticated", () => {
42
- expect(verifyUser(sampleCookie, publicKey, new Date(0), guardianValidation).status).toBe(AuthenticationStatus.AUTHORISED);
115
+ test("fail to authenticate with invalid-cookie reason if cookie has multiple dot separators", () => {
116
+ const expected: CookieFailure = {
117
+ success: false,
118
+ reason: 'invalid-cookie'
119
+ };
120
+ const multipleDotsCookie = sampleCookie.replace(".", "..");
121
+ expect(verifyUser(multipleDotsCookie, publicKey, new Date(0), guardianValidation)).toStrictEqual(expected);
122
+ });
123
+
124
+ test("authenticate if the cookie and user are valid", () => {
125
+ const expected: FreshSuccess = {
126
+ success: true,
127
+ // Cookie is not expired so no need to refresh credentials
128
+ shouldRefreshCredentials: false,
129
+ user: userFromCookie(sampleCookie)
130
+ };
131
+ expect(verifyUser(sampleCookie, publicKey, new Date(0), guardianValidation)).toStrictEqual(expected);
132
+ });
133
+
134
+ test("authenticate with shouldRefreshCredentials if cookie expired but we're within the grace period", () => {
135
+ const beforeEndOfGracePeriod = new Date(1234 + gracePeriodInMillis - 1);
136
+ const expected: StaleSuccess = {
137
+ success: true,
138
+ user: userFromCookie(sampleCookie),
139
+ shouldRefreshCredentials: true,
140
+ mustRefreshByEpochTimeMillis: 1234 + gracePeriodInMillis
141
+ };
142
+ expect(verifyUser(sampleCookie, publicKey, beforeEndOfGracePeriod, guardianValidation)).toStrictEqual(expected);
43
143
  });
44
144
  });
45
145
 
@@ -122,32 +222,133 @@ describe('panda class', function () {
122
222
  (fetchPublicKey as jest.MockedFunction<typeof fetchPublicKey>).mockResolvedValue({ key: publicKey, lastUpdated: new Date() });
123
223
  });
124
224
 
125
- it('should return authenticated if valid', async () => {
225
+ it('should authenticate if cookie and user are valid', async () => {
126
226
  jest.setSystemTime(100);
127
227
  const panda = new PanDomainAuthentication('cookiename', 'region', 'bucket', 'keyfile', (u)=> true);
128
- const { status } = await panda.verify(`cookiename=${sampleCookie}`);
228
+ const authenticationResult = await panda.verify(`cookiename=${sampleCookie}`);
229
+
230
+ const expected: FreshSuccess = {
231
+ success: true,
232
+ // Cookie is not expired
233
+ shouldRefreshCredentials: false,
234
+ user: userFromCookie(sampleCookie)
235
+ }
236
+ expect(authenticationResult).toStrictEqual(expected);
237
+ });
129
238
 
130
- expect(status).toBe(AuthenticationStatus.AUTHORISED);
239
+ it('should authenticate if cookie and user are valid when multiple cookies are passed', async () => {
240
+ jest.setSystemTime(100);
241
+ const panda = new PanDomainAuthentication('cookiename', 'region', 'bucket', 'keyfile', (u)=> true);
242
+ const authenticationResult = await panda.verify(`a=blah; b=stuff; cookiename=${sampleCookie}; c=4958345`);
243
+
244
+ const expected: FreshSuccess = {
245
+ success: true,
246
+ // Cookie is not expired
247
+ shouldRefreshCredentials: false,
248
+ user: userFromCookie(sampleCookie)
249
+ };
250
+ expect(authenticationResult).toStrictEqual(expected);
131
251
  });
132
252
 
133
- it('should return expired if expired', async () => {
134
- jest.setSystemTime(10_000);
253
+ it('should fail to authenticate if cookie expired and we\'re outside the grace period', async () => {
254
+ // Cookie expiry is 1234
255
+ const afterEndOfGracePeriodEpochMillis = 1234 + gracePeriodInMillis + 1
256
+ jest.setSystemTime(afterEndOfGracePeriodEpochMillis);
135
257
 
136
258
  const panda = new PanDomainAuthentication('cookiename', 'region', 'bucket', 'keyfile', (u)=> true);
137
- const { status } = await panda.verify(`cookiename=${sampleCookie}`);
259
+ const authenticationResult = await panda.verify(`cookiename=${sampleCookie}`);
138
260
 
139
- expect(status).toBe(AuthenticationStatus.EXPIRED);
261
+ const expected: CookieFailure = {
262
+ success: false,
263
+ reason: 'expired-cookie'
264
+ };
265
+ expect(authenticationResult).toStrictEqual(expected);
140
266
  });
141
267
 
142
- it('should return not authenticated if validation fails', async () => {
268
+ it('authenticate with shouldRefreshCredentials if cookie expired but we\'re within the grace period', async () => {
269
+ // Cookie expiry is 1234
270
+ const beforeEndOfGracePeriodEpochMillis = 1234 + gracePeriodInMillis - 1;
271
+ jest.setSystemTime(beforeEndOfGracePeriodEpochMillis);
272
+
273
+ const panda = new PanDomainAuthentication('cookiename', 'region', 'bucket', 'keyfile', (u)=> true);
274
+ const authenticationResult = await panda.verify(`cookiename=${sampleCookie}`);
275
+
276
+ const expected: StaleSuccess = {
277
+ success: true,
278
+ shouldRefreshCredentials: true,
279
+ mustRefreshByEpochTimeMillis: 1234 + gracePeriodInMillis,
280
+ user: userFromCookie(sampleCookie)
281
+ };
282
+ expect(authenticationResult).toStrictEqual(expected);
283
+ });
284
+
285
+ it('should fail to authenticate if user is not valid', async () => {
143
286
  jest.setSystemTime(100);
144
287
 
145
288
  const panda = new PanDomainAuthentication('cookiename', 'region', 'bucket', 'keyfile', guardianValidation);
146
- const { status } = await panda.verify(`cookiename=${sampleNonGuardianCookie}`);
289
+ const authenticationResult = await panda.verify(`cookiename=${sampleNonGuardianCookie}`);
290
+
291
+ const expected: UserValidationFailure = {
292
+ success: false,
293
+ reason: 'invalid-user',
294
+ user: userFromCookie(sampleNonGuardianCookie)
295
+ };
296
+ expect(authenticationResult).toStrictEqual(expected);
297
+ });
147
298
 
148
- expect(status).toBe(AuthenticationStatus.NOT_AUTHORISED);
299
+ it('should fail to authenticate if there is no cookie with the correct name', async () => {
300
+ jest.setSystemTime(100);
301
+
302
+ const panda = new PanDomainAuthentication('cookiename', 'region', 'bucket', 'keyfile', guardianValidation);
303
+ const authenticationResult = await panda.verify(`wrongcookiename=${sampleNonGuardianCookie}`);
304
+
305
+ const expected: CookieFailure = {
306
+ success: false,
307
+ reason: "no-cookie"
308
+ };
309
+ expect(authenticationResult).toStrictEqual(expected);
149
310
  });
150
311
 
312
+ it('should fail to authenticate if the cookie request header is malformed', async () => {
313
+ jest.setSystemTime(100);
314
+
315
+ const panda = new PanDomainAuthentication('cookiename', 'region', 'bucket', 'keyfile', guardianValidation);
316
+ // The cookie headers should be semicolon-separated name=valueg
317
+ const authenticationResult = await panda.verify(sampleNonGuardianCookie);
318
+
319
+ const expected: CookieFailure = {
320
+ success: false,
321
+ reason: "no-cookie"
322
+ };
323
+ expect(authenticationResult).toStrictEqual(expected);
324
+ });
325
+
326
+ it('should fail to authenticate if there is no cookie with the correct name out of multiple cookies', async () => {
327
+ jest.setSystemTime(100);
328
+
329
+ const panda = new PanDomainAuthentication('cookiename', 'region', 'bucket', 'keyfile', guardianValidation);
330
+ const authenticationResult = await panda.verify(`wrongcookiename=${sampleNonGuardianCookie}; anotherwrongcookiename=${sampleNonGuardianCookie}`);
331
+
332
+ const expected: CookieFailure = {
333
+ success: false,
334
+ reason: "no-cookie"
335
+ };
336
+ expect(authenticationResult).toStrictEqual(expected);
337
+ });
338
+
339
+ it('should fail to authenticate with invalid-cookie reason if cookie is malformed', async () => {
340
+ jest.setSystemTime(100);
341
+
342
+ const panda = new PanDomainAuthentication('rightcookiename', 'region', 'bucket', 'keyfile', guardianValidation);
343
+ // There is a valid Panda cookie in here, but it's under the wrong name
344
+ const authenticationResult = await panda.verify(`wrongcookiename=${sampleNonGuardianCookie}; rightcookiename=not-valid-panda-cookie`);
345
+
346
+ const expected: CookieFailure = {
347
+ success: false,
348
+ reason: "invalid-cookie"
349
+ };
350
+ expect(authenticationResult).toStrictEqual(expected);
351
+ });
151
352
  });
152
353
 
153
354
  });
@@ -3,11 +3,17 @@ import { sampleCookie } from './fixtures';
3
3
  import { URLSearchParams } from 'url';
4
4
 
5
5
  test("decode a cookie", () => {
6
- const { data, signature } = parseCookie(sampleCookie);
7
- expect(signature.length).toBe(684);
6
+ const parsedCookie = parseCookie(sampleCookie);
7
+ expect(parsedCookie).toBeDefined()
8
8
 
9
- const params = new URLSearchParams(data);
10
-
11
- expect(params.get("firstName")).toBe("Test");
12
- expect(params.get("lastName")).toBe("User");
9
+ // Unfortunately the above expect() doesn't narrow the type
10
+ if (parsedCookie) {
11
+ const { data, signature } = parsedCookie;
12
+ expect(signature.length).toBe(684);
13
+
14
+ const params = new URLSearchParams(data);
15
+
16
+ expect(params.get("firstName")).toBe("Test");
17
+ expect(params.get("lastName")).toBe("User");
18
+ }
13
19
  });
@@ -1,19 +0,0 @@
1
- name: Snyk
2
-
3
- on:
4
- push:
5
- branches:
6
- - main
7
- workflow_dispatch:
8
-
9
- jobs:
10
- security:
11
- uses: guardian/.github/.github/workflows/sbt-node-snyk.yml@main
12
- with:
13
- DEBUG: true
14
- ORG: guardian
15
- SKIP_NODE: false
16
- NODE_VERSION_FILE: .nvmrc
17
-
18
- secrets:
19
- SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}