Skip to content

Commit c4aef9b

Browse files
authored
Merge pull request #65 from bartstc/chore/static-code-analysis
chore: setup SNYK and sonar cube
2 parents 5dc4ec1 + 96c3cdf commit c4aef9b

11 files changed

Lines changed: 166 additions & 22 deletions

File tree

.github/workflows/ci.yml

Lines changed: 122 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,46 @@ on:
44
push:
55
branches:
66
- core
7-
- basic
87
pull_request:
98
branches:
109
- core
11-
- basic
1210

1311
env:
1412
NODE_VERSION: "22.x"
1513

14+
# ----- Static analysis toggles & config -----
15+
# Flip to "false" to disable a tool.
16+
SNYK_ENABLED: "true"
17+
SONARCLOUD_ENABLED: "true"
18+
19+
# The branch on which static-analysis jobs run (push or PR target).
20+
# Fork users: change to your default branch (e.g. "main").
21+
STATIC_ANALYSIS_BRANCH: "core"
22+
23+
# Fork users: replace these with your own SonarCloud org slug and project key.
24+
# Find the project key in SonarCloud:
25+
# - the `id=...` value in the project page URL, OR
26+
# - Project -> Administration -> Information -> Key.
27+
# GitHub-imported projects typically use "<org>_<repo>"; manually-created ones may differ.
28+
SONAR_ORGANIZATION: "bartstc"
29+
SONAR_PROJECT_KEY: "bartstc_vite-ts-react-template"
30+
1631
jobs:
32+
# Exposes workflow-level env vars as outputs so they can be referenced in
33+
# job-level `if:` expressions (the `env` context is not available there).
34+
config:
35+
runs-on: ubuntu-latest
36+
outputs:
37+
snyk_enabled: ${{ steps.set.outputs.snyk_enabled }}
38+
sonarcloud_enabled: ${{ steps.set.outputs.sonarcloud_enabled }}
39+
static_analysis_branch: ${{ steps.set.outputs.static_analysis_branch }}
40+
steps:
41+
- id: set
42+
run: |
43+
echo "snyk_enabled=$SNYK_ENABLED" >> "$GITHUB_OUTPUT"
44+
echo "sonarcloud_enabled=$SONARCLOUD_ENABLED" >> "$GITHUB_OUTPUT"
45+
echo "static_analysis_branch=$STATIC_ANALYSIS_BRANCH" >> "$GITHUB_OUTPUT"
46+
1747
lint-tests:
1848
uses: ./.github/workflows/run-tests.yml
1949
with:
@@ -163,6 +193,96 @@ jobs:
163193
name: storybook-coverage-report
164194
path: coverage/**/*
165195

196+
snyk:
197+
needs: config
198+
if: >
199+
${{ needs.config.outputs.snyk_enabled == 'true'
200+
&& (github.ref_name == needs.config.outputs.static_analysis_branch
201+
|| github.base_ref == needs.config.outputs.static_analysis_branch) }}
202+
runs-on: ubuntu-latest
203+
permissions:
204+
contents: read
205+
security-events: write
206+
steps:
207+
- name: Checkout
208+
uses: actions/checkout@v4
209+
210+
- name: Enable corepack
211+
run: corepack enable
212+
213+
- name: Setup Node.js
214+
uses: actions/setup-node@v4
215+
with:
216+
node-version: ${{ env.NODE_VERSION }}
217+
cache: "pnpm"
218+
219+
- name: Install dependencies
220+
run: pnpm install --no-frozen-lockfile --ignore-scripts
221+
222+
- name: Install Snyk CLI
223+
run: npm install -g snyk
224+
225+
- name: Snyk Open Source (SCA)
226+
env:
227+
SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
228+
run: |
229+
snyk test \
230+
--all-projects \
231+
--strict-out-of-sync=false \
232+
--severity-threshold=high \
233+
--sarif-file-output=snyk-deps.sarif
234+
235+
- name: Snyk Code (SAST)
236+
env:
237+
SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
238+
run: |
239+
snyk code test \
240+
--severity-threshold=high \
241+
--sarif-file-output=snyk-code.sarif
242+
243+
- name: Upload Snyk SARIF to GitHub Security
244+
if: always()
245+
uses: github/codeql-action/upload-sarif@v3
246+
with:
247+
sarif_file: .
248+
category: snyk
249+
250+
sonarcloud:
251+
needs: [config, unit-tests, storybook-tests]
252+
if: >
253+
${{ needs.config.outputs.sonarcloud_enabled == 'true'
254+
&& (github.ref_name == needs.config.outputs.static_analysis_branch
255+
|| github.base_ref == needs.config.outputs.static_analysis_branch) }}
256+
runs-on: ubuntu-latest
257+
steps:
258+
- name: Checkout
259+
uses: actions/checkout@v4
260+
with:
261+
fetch-depth: 0 # full history for blame & new-code detection
262+
263+
- name: Download coverage artifacts
264+
uses: actions/download-artifact@v4
265+
with:
266+
pattern: "*-coverage-report"
267+
268+
- name: Rewrite LCOV source paths for Sonar
269+
run: |
270+
# vitest's `projectRoot: "./src"` makes SF: paths relative to src/.
271+
# Sonar expects paths relative to repo root, so prepend src/.
272+
sed -i 's|^SF:|SF:src/|' \
273+
unit-coverage-report/lcov.info \
274+
storybook-coverage-report/lcov.info
275+
276+
- name: SonarCloud Scan
277+
uses: SonarSource/sonarqube-scan-action@v4
278+
env:
279+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
280+
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
281+
with:
282+
args: >
283+
-Dsonar.projectKey=${{ env.SONAR_PROJECT_KEY }}
284+
-Dsonar.organization=${{ env.SONAR_ORGANIZATION }}
285+
166286
build:
167287
needs:
168288
[

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ An opinionated, production-ready starter for **Single Page Application** develop
1313
- [PNPM](https://pnpm.io/) — fast, disk-efficient package manager
1414
- [Devcontainer](https://code.visualstudio.com/docs/devcontainers/containers) — reproducible VS Code dev environment
1515
- [GitHub Actions](https://docs.github.com/en/actions) CI — tests, build, coverage reports, deploy draft
16+
- [Snyk](https://snyk.io/) — dependency (SCA) and source code (SAST) security scanning, with SARIF results in the GitHub Security tab
17+
- [SonarCloud](https://sonarcloud.io/) — code quality, maintainability, and coverage gating on pull requests
1618
- [GitHub Copilot](https://github.com/features/copilot) — instructions, skills, and custom prompts (`.github/instructions/`, `.github/skills/`, `.github/prompts/`)
1719
- [Claude Code](https://docs.anthropic.com/en/docs/claude-code/overview) — project rules (`CLAUDE.md`), skills, subagents, and custom commands (`.claude/`)
1820

package.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,5 +118,10 @@
118118
"msw": {
119119
"workerDirectory": "public"
120120
},
121+
"pnpm": {
122+
"overrides": {
123+
"@fastify/static": "9.1.1"
124+
}
125+
},
121126
"packageManager": "pnpm@10.13.1"
122127
}

server/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
"@fastify/swagger": "9.7.0",
1414
"@fastify/swagger-ui": "5.2.5",
1515
"@fastify/jwt": "10.0.0",
16-
"fastify": "5.8.4",
16+
"fastify": "5.8.5",
1717
"lowdb": "7.0.1"
1818
},
1919
"devDependencies": {

server/src/config.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@ import path from "path";
44
const __dirname = path.dirname(fileURLToPath(import.meta.url));
55

66
export const PORT = 3001;
7-
export const JWT_SECRET = "local-dev-secret-do-not-use-in-production";
7+
export const JWT_SECRET =
8+
process.env.JWT_SECRET ?? "local-dev-secret-do-not-use-in-production";
89
export const DB_PATH = path.resolve(__dirname, "db/db.json");
910

1011
// AIDEV-NOTE: set to false to disable auth on all mutation routes (dev convenience)

sonar-project.properties

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
sonar.sources=src,server/src
2+
sonar.tests=src,e2e
3+
4+
# Treat tests/stories as test code, not source
5+
sonar.test.inclusions=**/*.test.ts,**/*.test.tsx,**/*.spec.ts,**/*.stories.tsx
6+
7+
# Skip generated/config noise
8+
sonar.exclusions=**/node_modules/**,**/dist/**,**/coverage/**,**/coverage-report/**,**/*.config.{ts,js,mjs},**/*.d.ts,public/**,reports/**
9+
10+
# Coverage from vitest (paths post-sed in CI: see ci.yml `Rewrite LCOV source paths for Sonar`)
11+
sonar.javascript.lcov.reportPaths=unit-coverage-report/lcov.info,storybook-coverage-report/lcov.info
12+
sonar.typescript.tsconfigPath=tsconfig.json
13+
14+
sonar.sourceEncoding=UTF-8

src/features/auth/application/auth-store.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,16 @@
11
import { createContext, useContext } from "react";
22
import { createStore, useStore } from "zustand";
33

4+
import { IS_AUTHENTICATED_STORAGE } from "@/features/auth/models/storage-keys";
45
import type { User } from "@/features/auth/models/user";
56
import { getUser } from "@/features/auth/providers/get-user";
67
import { AUTH_TOKEN_KEY } from "@/lib/http/ky-client";
78

89
import { loginUser, type ICredentials } from "../providers/login-user";
910

10-
const AUTH_KEY = "fake_store_is_authenticated";
11-
1211
// could be also https://www.npmjs.com/package/zustand-persist lib for advanced use cases
13-
const isLoggedIn = () => localStorage.getItem(AUTH_KEY) === "true";
12+
const isLoggedIn = () =>
13+
localStorage.getItem(IS_AUTHENTICATED_STORAGE) === "true";
1414

1515
interface IStore {
1616
isAuthenticated: boolean;
@@ -72,15 +72,15 @@ export const initializeAuthStore = (preloadedState: Partial<IStore> = {}) => {
7272
localStorage.setItem(AUTH_TOKEN_KEY, token);
7373
const user = await getUser();
7474

75-
localStorage.setItem(AUTH_KEY, "true");
75+
localStorage.setItem(IS_AUTHENTICATED_STORAGE, "true");
7676

7777
set({
7878
isAuthenticated: true,
7979
state: "finished",
8080
user,
8181
});
8282
} catch (e) {
83-
localStorage.setItem(AUTH_KEY, "false");
83+
localStorage.setItem(IS_AUTHENTICATED_STORAGE, "false");
8484

8585
set({
8686
isAuthenticated: false,
@@ -97,7 +97,7 @@ export const initializeAuthStore = (preloadedState: Partial<IStore> = {}) => {
9797
});
9898

9999
return new Promise((resolve) => setTimeout(resolve, 500)).then(() => {
100-
localStorage.setItem(AUTH_KEY, "false");
100+
localStorage.setItem(IS_AUTHENTICATED_STORAGE, "false");
101101
localStorage.removeItem(AUTH_TOKEN_KEY);
102102
set({
103103
isAuthenticated: false,
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export const IS_AUTHENTICATED_STORAGE = "fake_store_is_authenticated";

src/features/authv2/application/AuthProvider.tsx

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import type { PropsWithChildren } from "react";
44
import { useEffect } from "react";
55
import { fromPromise } from "xstate";
66

7+
import { IS_AUTHENTICATED_STORAGE } from "@/features/auth/models/storage-keys";
78
import { getUser } from "@/features/auth/providers/get-user";
89
import { loginUser } from "@/features/auth/providers/login-user";
910
import { AuthContext } from "@/features/authv2/application/auth-context";
@@ -15,16 +16,16 @@ import {
1516
import { getRoles } from "@/features/authv2/providers/get-roles";
1617
import { sleep } from "@/lib/sleep";
1718

18-
const AUTH_KEY = "fake_store_is_authenticated";
19-
2019
export const AuthProvider = ({ children }: PropsWithChildren) => {
2120
const checkAuthStatus = () => {
22-
return Promise.resolve(localStorage.getItem(AUTH_KEY) === "true");
21+
return Promise.resolve(
22+
localStorage.getItem(IS_AUTHENTICATED_STORAGE) === "true"
23+
);
2324
};
2425

2526
const logout = async () => {
2627
await sleep(500);
27-
localStorage.setItem(AUTH_KEY, "false");
28+
localStorage.setItem(IS_AUTHENTICATED_STORAGE, "false");
2829
};
2930

3031
const authActor = useActorRef(
@@ -34,7 +35,7 @@ export const AuthProvider = ({ children }: PropsWithChildren) => {
3435
getUser: fromPromise(getUser),
3536
loginUser: fromPromise(async ({ input }) => {
3637
await loginUser(input);
37-
localStorage.setItem(AUTH_KEY, "true");
38+
localStorage.setItem(IS_AUTHENTICATED_STORAGE, "true");
3839
}),
3940
getRoles: fromPromise(getRoles),
4041
logout: fromPromise(logout),

src/features/authv2/application/storage-machine.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { assign, fromPromise, setup } from "xstate";
22

3-
const AUTH_KEY = "fake_store_is_authenticated";
3+
import { IS_AUTHENTICATED_STORAGE } from "@/features/auth/models/storage-keys";
44

55
interface StorageMachineContext {
66
isAuthenticated: boolean;
@@ -13,7 +13,9 @@ type StorageMachineEvents =
1313
export type StorageMachineType = typeof storageMachine;
1414

1515
const checkAuthStatus = fromPromise(() => {
16-
return Promise.resolve(localStorage.getItem(AUTH_KEY) === "true");
16+
return Promise.resolve(
17+
localStorage.getItem(IS_AUTHENTICATED_STORAGE) === "true"
18+
);
1719
});
1820

1921
export const storageMachine = setup({
@@ -27,11 +29,11 @@ export const storageMachine = setup({
2729
actions: {
2830
setAuthStorage: ({ event }) => {
2931
if (event.type === "SET_AUTHENTICATED") {
30-
localStorage.setItem(AUTH_KEY, String(event.value));
32+
localStorage.setItem(IS_AUTHENTICATED_STORAGE, String(event.value));
3133
}
3234
},
3335
clearAuthStorage: () => {
34-
localStorage.setItem(AUTH_KEY, "false");
36+
localStorage.setItem(IS_AUTHENTICATED_STORAGE, "false");
3537
},
3638
},
3739
}).createMachine({

0 commit comments

Comments
 (0)