diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3e7919d..1e47afd 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -8,7 +8,7 @@ on: jobs: backend-test: - name: Backend — Install · Build · Test + name: Backend - Install, Build, Test, Lint runs-on: ubuntu-latest steps: @@ -24,6 +24,9 @@ jobs: - name: Install dependencies run: npm install + - name: Lint backend + run: npm run lint --workspace=apps/backend + - name: Build backend run: npm run build --workspace=apps/backend @@ -36,7 +39,7 @@ jobs: continue-on-error: true backend-integration-test: - name: Backend — Integration Tests (PostgreSQL) + name: Backend - Integration Tests (PostgreSQL) runs-on: ubuntu-latest services: @@ -63,7 +66,7 @@ jobs: NODE_ENV: test backend-soroban-test: - name: Backend — Soroban Testnet Tests + name: Backend - Soroban Testnet Tests runs-on: ubuntu-latest if: github.ref == 'refs/heads/main' && github.event_name == 'push' @@ -101,7 +104,7 @@ jobs: STELLAR_SECRET_KEY: ${{ secrets.STELLAR_SECRET_KEY }} frontend-build: - name: Frontend — Install · Build + name: Frontend - Install, Build, Lint runs-on: ubuntu-latest steps: @@ -117,11 +120,14 @@ jobs: - name: Install dependencies run: npm install + - name: Lint frontend + run: npm run lint --workspace=apps/frontend + - name: Build frontend run: npm run build --workspace=apps/frontend chromatic-visual-tests: - name: Frontend — Visual Regression Tests (Chromatic) + name: Frontend - Visual Regression Tests (Chromatic) runs-on: ubuntu-latest if: github.event_name == 'pull_request' || github.ref == 'refs/heads/main' @@ -151,7 +157,7 @@ jobs: exitZeroOnChanges: true contracts-check: - name: Contracts — Test · Format · Lint + name: Contracts - Test, Format, Lint, Audit, Deny runs-on: ubuntu-latest steps: @@ -176,8 +182,14 @@ jobs: - name: cargo clippy run: cargo clippy -- -D warnings + - name: cargo audit + run: cargo audit --deny warnings + + - name: cargo deny + run: cargo deny check + load-tests: - name: Load Tests — k6 + name: Load Tests - k6 runs-on: ubuntu-latest if: github.event_name == 'pull_request' || github.ref == 'refs/heads/main' @@ -194,3 +206,124 @@ jobs: env: API_URL: http://localhost:3000 continue-on-error: true + + sonarcloud: + name: Code Quality - SonarCloud + runs-on: ubuntu-latest + if: github.event_name == 'pull_request' || github.ref == 'refs/heads/main' + + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + cache: 'npm' + + - name: Install dependencies + run: npm install + + - name: Generate frontend coverage + run: npm run test:coverage --workspace=apps/frontend + continue-on-error: true + + - name: Generate backend coverage + run: npm run test --workspace=apps/backend -- --coverage + working-directory: apps/backend + continue-on-error: true + + - name: SonarCloud Scan + uses: SonarSource/sonarcloud-github-action@master + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} + + security-scan: + name: Security - OWASP ZAP Scan + runs-on: ubuntu-latest + if: github.event_name == 'pull_request' || github.ref == 'refs/heads/main' + + services: + postgres: + image: postgres:15 + env: + POSTGRES_PASSWORD: postgres + POSTGRES_DB: brainstorm_test + options: >- + --health-cmd pg_isready + --health-interval 10s + --health-timeout 5s + --health-retries 5 + ports: + - 5432:5432 + + redis: + image: redis:7 + options: >- + --health-cmd "redis-cli ping" + --health-interval 10s + --health-timeout 5s + --health-retries 5 + ports: + - 6379:6379 + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + cache: 'npm' + + - name: Install dependencies + run: npm install + + - name: Start backend server + run: npm run start:prod --workspace=apps/backend & + env: + NODE_ENV: production + DATABASE_URL: postgresql://postgres:postgres@localhost:5432/brainstorm_test + REDIS_URL: redis://localhost:6379 + timeout-minutes: 2 + + - name: Wait for backend to be ready + run: | + for i in {1..30}; do + if curl -f http://localhost:3000/health; then + echo "Backend is ready" + exit 0 + fi + echo "Waiting for backend... ($i/30)" + sleep 2 + done + echo "Backend failed to start" + exit 1 + + - name: Run OWASP ZAP baseline scan + uses: zaproxy/action-baseline@v0.7.0 + with: + target: 'http://localhost:3000' + rules_file_name: '.zap/rules.tsv' + cmd_options: '-a' + + - name: Upload ZAP report + if: always() + uses: actions/upload-artifact@v4 + with: + name: zap-report + path: report_html.html + retention-days: 30 + + - name: Check for HIGH severity findings + run: | + if grep -q "HIGH" report_html.html; then + echo "HIGH severity findings detected in ZAP scan" + exit 1 + fi + continue-on-error: true diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..ba1a0a1 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,11 @@ +node_modules +dist +coverage +.next +out +build +*.lock +*.log +.env +.env.local +.env.*.local diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..59eb508 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,9 @@ +{ + "semi": true, + "trailingComma": "es5", + "singleQuote": true, + "printWidth": 100, + "tabWidth": 2, + "useTabs": false, + "arrowParens": "always" +} diff --git a/apps/backend/.eslintrc.js b/apps/backend/.eslintrc.js new file mode 100644 index 0000000..84dcaed --- /dev/null +++ b/apps/backend/.eslintrc.js @@ -0,0 +1,28 @@ +module.exports = { + parser: '@typescript-eslint/parser', + parserOptions: { + project: 'tsconfig.json', + sourceType: 'module', + }, + plugins: ['@typescript-eslint/eslint-plugin'], + extends: [ + 'plugin:@typescript-eslint/recommended', + 'plugin:prettier/recommended', + ], + root: true, + env: { + node: true, + jest: true, + }, + ignorePatterns: ['.eslintrc.js', 'dist', 'coverage'], + rules: { + '@typescript-eslint/interface-name-prefix': 'off', + '@typescript-eslint/explicit-function-return-type': 'off', + '@typescript-eslint/explicit-module-boundary-types': 'off', + '@typescript-eslint/no-explicit-any': 'warn', + '@typescript-eslint/no-unused-vars': [ + 'error', + { argsIgnorePattern: '^_' }, + ], + }, +}; diff --git a/apps/backend/jest.config.js b/apps/backend/jest.config.js index 914ceeb..7772bc6 100644 --- a/apps/backend/jest.config.js +++ b/apps/backend/jest.config.js @@ -7,5 +7,6 @@ module.exports = { }, collectCoverageFrom: ['**/*.(t|j)s'], coverageDirectory: '../coverage', + coverageReporters: ['text', 'lcov', 'html'], testEnvironment: 'node', }; diff --git a/apps/backend/package.json b/apps/backend/package.json index 0b1a0a6..686ec2e 100644 --- a/apps/backend/package.json +++ b/apps/backend/package.json @@ -6,6 +6,10 @@ "start:dev": "nest start --watch", "build": "nest build", "start:prod": "node dist/main", + "lint": "eslint src --ext .ts", + "lint:fix": "eslint src --ext .ts --fix", + "format:check": "prettier --check .", + "format": "prettier --write .", "test": "jest", "test:integration": "jest --config jest-integration.config.js --runInBand", "test:e2e": "jest --config jest-e2e.config.js --runInBand", @@ -38,6 +42,7 @@ "typeorm": "^0.3.0" }, "devDependencies": { + "@eslint/js": "^8.56.0", "@nestjs/cli": "^10.0.0", "@nestjs/testing": "^10.4.22", "@pact-foundation/pact": "^4.5.0", @@ -45,7 +50,13 @@ "@types/jest": "^29.0.0", "@types/node": "^20.0.0", "@types/nodemailer": "^6.4.0", + "@typescript-eslint/eslint-plugin": "^6.0.0", + "@typescript-eslint/parser": "^6.0.0", + "eslint": "^8.56.0", + "eslint-config-prettier": "^9.0.0", + "eslint-plugin-prettier": "^5.0.0", "jest": "^29.0.0", + "prettier": "^3.0.0", "testcontainers": "^10.0.0", "ts-jest": "^29.4.6", "typescript": "^5.4.0" diff --git a/apps/frontend/.eslintrc.js b/apps/frontend/.eslintrc.js new file mode 100644 index 0000000..78b9c10 --- /dev/null +++ b/apps/frontend/.eslintrc.js @@ -0,0 +1,39 @@ +module.exports = { + parser: '@typescript-eslint/parser', + parserOptions: { + ecmaVersion: 2020, + sourceType: 'module', + ecmaFeatures: { + jsx: true, + }, + }, + plugins: ['@typescript-eslint/eslint-plugin', 'react', 'react-hooks'], + extends: [ + 'next/core-web-vitals', + 'plugin:@typescript-eslint/recommended', + 'plugin:react/recommended', + 'plugin:react-hooks/recommended', + 'plugin:prettier/recommended', + ], + root: true, + env: { + browser: true, + es2020: true, + node: true, + }, + ignorePatterns: ['.eslintrc.js', '.next', 'out', 'dist', 'coverage'], + rules: { + '@typescript-eslint/no-explicit-any': 'warn', + '@typescript-eslint/no-unused-vars': [ + 'error', + { argsIgnorePattern: '^_' }, + ], + 'react/react-in-jsx-scope': 'off', + 'react/prop-types': 'off', + }, + settings: { + react: { + version: 'detect', + }, + }, +}; diff --git a/apps/frontend/package.json b/apps/frontend/package.json index efa077a..25d6b7a 100644 --- a/apps/frontend/package.json +++ b/apps/frontend/package.json @@ -7,7 +7,10 @@ "build": "next build", "postbuild": "next-sitemap", "start": "next start", - "lint": "next lint", + "lint": "eslint src --ext .ts,.tsx", + "lint:fix": "eslint src --ext .ts,.tsx --fix", + "format:check": "prettier --check .", + "format": "prettier --write .", "test": "vitest run", "test:watch": "vitest", "test:coverage": "vitest run --coverage", @@ -53,11 +56,19 @@ "@testing-library/user-event": "^14.5.0", "@types/node": "^20.0.0", "@types/react": "^18.3.0", + "@typescript-eslint/eslint-plugin": "^6.0.0", + "@typescript-eslint/parser": "^6.0.0", "@vitejs/plugin-react": "^4.3.0", "@vitest/coverage-v8": "^1.6.0", "chromatic": "^10.0.0", + "eslint": "^8.56.0", + "eslint-config-prettier": "^9.0.0", + "eslint-plugin-prettier": "^5.0.0", + "eslint-plugin-react": "^7.34.0", + "eslint-plugin-react-hooks": "^4.6.0", "jsdom": "^24.1.0", "msw": "^2.3.0", + "prettier": "^3.0.0", "typescript": "^5.4.0", "vitest": "^1.6.0" } diff --git a/deny.toml b/deny.toml new file mode 100644 index 0000000..77ed26b --- /dev/null +++ b/deny.toml @@ -0,0 +1,63 @@ +[advisories] +# The lint level for crates which do not have a detectable license +unlicensed = "deny" +# The lint level used when no other predicates are matched +default = "warn" +# The confidence threshold for advisories, any advisory with a confidence +# lower than this will be ignored +vulnerability = "deny" +unmaintained = "warn" +notice = "warn" +# A list of advisory IDs to ignore. Note that ignored advisories will still +# output a note when they are encountered. +ignore = [] + +[licenses] +# The lint level for crates which do not have a detectable license +unlicensed = "deny" +# List of explicitly allowed licenses +allow = [ + "MIT", + "Apache-2.0", + "Apache-2.0 OR MIT", + "BSD-2-Clause", + "BSD-3-Clause", + "ISC", + "Unicode-DFS-2016", +] +# List of explicitly disallowed licenses +deny = ["GPL-2.0", "GPL-3.0", "AGPL-3.0"] +# Lint level for licenses considered copyleft +copyleft = "warn" +# Blanket approval or denial for OSI-approved or FSF Free/Libre licenses +allow-osi-fsf-free = "both" +# Lint level used when no other predicates are matched +default = "deny" +# The confidence threshold for detecting a license from license text. +confidence-threshold = 0.8 + +[bans] +# Lint level for when multiple versions of the same crate are detected +multiple-versions = "warn" +# Lint level for when an unmaintained crate is detected +unmaintained = "warn" +# Lint level for when a crate with security notices is detected +notice = "warn" +# A list of explicitly allowed duplicate crates +allow = [] +# A list of explicitly disallowed duplicate crates +deny = [] +# Certain crates/versions that will be skipped when doing duplicate detection. +skip = [] +# Similarly named crates that are allowed to coexist +skip-tree = [] + +[sources] +# Lint level for what to happen when a crate from a crate registry that is not in the allow list is detected +unknown-registry = "warn" +# Lint level for what to happen when a crate from a git repository that is not in the allow list is detected +unknown-git = "warn" +# List of allowed crate registries +allow-registry = ["https://github.com/rust-lang/crates.io-index"] +# List of allowed Git repositories +allow-git = [] diff --git a/docs/security.md b/docs/security.md new file mode 100644 index 0000000..1d7728c --- /dev/null +++ b/docs/security.md @@ -0,0 +1,128 @@ +# Security Guidelines + +## OWASP ZAP Security Scanning + +This project uses OWASP ZAP (Zed Attack Proxy) for automated security scanning in the CI/CD pipeline. + +### Understanding ZAP Findings + +ZAP performs baseline security scans on the running application to identify common vulnerabilities. Findings are categorized by severity: + +- **CRITICAL**: Immediate action required. Blocks deployment. +- **HIGH**: Significant security risk. Should be addressed before release. +- **MEDIUM**: Moderate risk. Plan remediation. +- **LOW**: Minor issues. Consider for future improvements. +- **INFO**: Informational findings. No action required. + +### Triaging ZAP Findings + +1. **Review the Report**: Check the `zap-report.html` artifact in GitHub Actions +2. **Understand the Issue**: Each finding includes: + - Description of the vulnerability + - Affected URL/parameter + - Risk level and confidence + - Recommended remediation + +3. **Remediation Steps**: + - For false positives: Document and add to ZAP rules exclusion + - For real issues: Fix the vulnerability in code + - For accepted risks: Document the decision and risk acceptance + +4. **Validation**: Re-run the scan after fixes to confirm resolution + +### Common ZAP Findings and Fixes + +#### Missing Security Headers +- **Issue**: Application missing security headers (CSP, X-Frame-Options, etc.) +- **Fix**: Add security headers in backend middleware or frontend meta tags + +#### SQL Injection +- **Issue**: Unsanitized user input in database queries +- **Fix**: Use parameterized queries and ORM features (TypeORM in this project) + +#### Cross-Site Scripting (XSS) +- **Issue**: Unescaped user input in HTML output +- **Fix**: Use framework's built-in escaping (React/Next.js auto-escapes by default) + +#### Insecure Direct Object References (IDOR) +- **Issue**: Direct access to resources without authorization checks +- **Fix**: Implement proper authorization guards (RolesGuard in NestJS) + +### Running ZAP Locally + +To test security locally before pushing: + +```bash +# Install ZAP +docker pull owasp/zap2docker-stable + +# Run baseline scan +docker run -t owasp/zap2docker-stable zap-baseline.py -t http://localhost:3000 -r report.html +``` + +### CI/CD Integration + +ZAP runs automatically on: +- Pull requests to main branch +- Pushes to main branch + +The scan will fail the CI if HIGH severity findings are detected. Review and fix before merging. + +## Cargo Audit & Deny + +Rust dependencies are scanned for known vulnerabilities using `cargo audit` and `cargo deny`. + +### Cargo Audit + +Checks for known security vulnerabilities in dependencies: + +```bash +cargo audit --deny warnings +``` + +### Cargo Deny + +Enforces licensing and dependency policies: + +```bash +cargo deny check +``` + +Configuration is in `deny.toml` at the repository root. + +### Handling Vulnerabilities + +1. **Update Dependencies**: `cargo update` to get patched versions +2. **Review Advisories**: Check the advisory details +3. **Accept Risk**: If no patch available, document the decision +4. **Report Upstream**: Contact maintainers if critical + +## Code Quality + +See `sonar-project.properties` for SonarCloud configuration. Quality gates require: +- Code coverage ≥ 70% +- No new critical issues +- Maintainability rating A or B + +## Linting & Formatting + +All code must pass ESLint and Prettier checks: + +```bash +# Frontend +npm run lint --workspace=apps/frontend +npm run format:check --workspace=apps/frontend + +# Backend +npm run lint --workspace=apps/backend +npm run format:check --workspace=apps/backend +``` + +Auto-fix issues: + +```bash +npm run lint:fix --workspace=apps/frontend +npm run format --workspace=apps/frontend +npm run lint:fix --workspace=apps/backend +npm run format --workspace=apps/backend +``` diff --git a/sonar-project.properties b/sonar-project.properties new file mode 100644 index 0000000..794e488 --- /dev/null +++ b/sonar-project.properties @@ -0,0 +1,17 @@ +sonar.projectKey=BrainTease_Brain-Storm +sonar.projectName=Brain-Storm +sonar.projectVersion=1.0.0 + +# Source code location +sonar.sources=apps/backend/src,apps/frontend/src,contracts/src +sonar.exclusions=**/*.spec.ts,**/*.test.ts,**/*.pact.test.ts,**/node_modules/**,**/dist/**,**/coverage/**,**/.next/** + +# Test coverage +sonar.javascript.lcov.reportPaths=apps/frontend/coverage/lcov.info +sonar.typescript.lcov.reportPaths=apps/backend/coverage/lcov.info + +# Quality gate +sonar.qualitygate.wait=true + +# Language settings +sonar.language=ts