Sample Audit Reports

See exactly what you get. These are real reports generated by our AI auditing engine.

Code Review Report

ScanMyCode.dev Code Audit (AUD-K8X2P4QR7MNJ)

February 12, 2026

Repository: acme-corp/webshop-frontend

Review Date: February 12, 2026

Reviewer: ScanMyCode.dev Code Audit (AUD-K8X2P4QR7MNJ)

Tech Stack: Next.js 15, React 19, TypeScript, Prisma, PostgreSQL, Tailwind CSS 4

Score

Overall Score: 68 / 100

The webshop frontend has a solid foundation with modern tooling and good TypeScript adoption. Component composition is generally well-done, and the use of Prisma for data access prevents most SQL injection risks. However, the application has several high-severity issues including a memory leak in the dashboard WebSocket, missing error boundaries that can crash the entire app, and hardcoded configuration values including an API key in source code. The codebase also lacks consistent error handling patterns and has a growing 600-line checkout component that needs decomposition.

Strengths

  • Good TypeScript adoption: Strict mode enabled, interfaces defined for all API responses and component props.
  • Prisma ORM usage: Prevents SQL injection and provides type-safe database queries throughout the application.
  • Consistent naming conventions: Components use PascalCase, hooks use use prefix, utilities use camelCase.
  • Proper React patterns: Effective use of useMemo and useCallback to prevent unnecessary re-renders in product grid.
  • Tailwind CSS organization: Custom design tokens defined in tailwind.config.ts, consistent spacing and color usage.

Findings Overview

#FindingSeverityLocation
1Memory leak in WebSocket useEffect hookHighsrc/pages/dashboard.tsx:67-84
2No React Error BoundariesHighsrc/App.tsx
3API key hardcoded in sourceHighsrc/config/app.ts:3
4Missing cleanup in event listenersMediumsrc/hooks/useResize.ts:12
5Monolithic 600-line checkout componentMediumsrc/pages/checkout.tsx
6Inconsistent error handling across API callsMediumMultiple files
7Missing loading states on data fetchesLowsrc/pages/products.tsx
8Unused imports and dead codeLowMultiple files

Detailed Findings

1. Memory leak in WebSocket useEffect hook

Severity: HighLocation: src/pages/dashboard.tsx:67-84

The dashboard component opens a WebSocket connection for real-time order updates but never closes it on unmount. Each time the user navigates away and back, a new connection is created while the previous one remains open, accumulating connections and event handlers over time.

Current code:

// src/pages/dashboard.tsx - No cleanup on unmount
useEffect(() => {
  const ws = new WebSocket('wss://api.example.com/orders');

  ws.onmessage = (event) => {
    setOrders(prev => [...prev, JSON.parse(event.data)]);
  };

  ws.onerror = (err) => {
    console.error('WebSocket error:', err);
  };

  // Missing return cleanup!
}, []);

Recommended fix:

useEffect(() => {
  const ws = new WebSocket('wss://api.example.com/orders');

  ws.onmessage = (event) => {
    setOrders(prev => [...prev, JSON.parse(event.data)]);
  };

  ws.onerror = (err) => {
    console.error('WebSocket error:', err);
  };

  return () => {
    ws.close();
  };
}, []);

Why this matters: Each navigation to and from the dashboard creates a new WebSocket without closing the previous one. After 10 navigations, 10 WebSocket connections are active simultaneously, each appending duplicate data to state. This causes increasing memory usage and can lead to browser tab crashes in long-running sessions.

2. No React Error Boundaries

Severity: HighLocation: src/App.tsx

The application has no ErrorBoundary components anywhere in the component tree. A runtime error in any component (e.g., accessing a property of undefined in a product card) will crash the entire application with a blank white screen.

Current code:

// src/App.tsx - No error boundary
function App() {
  return (
    <Router>
      <Routes>
        <Route path="/" element={<Home />} />
        <Route path="/products" element={<Products />} />
        <Route path="/checkout" element={<Checkout />} />
      </Routes>
    </Router>
  );
}

Recommended fix:

// src/components/ErrorBoundary.tsx
'use client'
import { Component, ReactNode } from 'react'

interface Props { children: ReactNode; fallback?: ReactNode }
interface State { hasError: boolean }

class ErrorBoundary extends Component<Props, State> {
  state: State = { hasError: false }

  static getDerivedStateFromError(): State {
    return { hasError: true }
  }

  componentDidCatch(error: Error, info: React.ErrorInfo) {
    console.error('ErrorBoundary caught:', error, info)
  }

  render() {
    if (this.state.hasError) {
      return this.props.fallback || <FallbackErrorPage />
    }
    return this.props.children
  }
}

// src/App.tsx - With error boundary
function App() {
  return (
    <ErrorBoundary>
      <Router>
        <Routes>
          <Route path="/" element={<Home />} />
          <Route path="/products" element={<Products />} />
          <Route path="/checkout" element={<Checkout />} />
        </Routes>
      </Router>
    </ErrorBoundary>
  );
}

Why this matters: Without error boundaries, a single rendering error in any component takes down the entire application. In an e-commerce context, a malformed product response from the API could make the checkout page inaccessible, directly impacting revenue.

3. API key hardcoded in source

Severity: HighLocation: src/config/app.ts:3

A Stripe API key is hardcoded directly in the configuration file. This key is committed to version control and will be included in the client-side JavaScript bundle, making it visible to anyone who inspects the browser DevTools.

Current code:

// src/config/app.ts
const config = {
  stripeKey: 'sk_live_51N8x2KLm4...',
  apiUrl: 'https://api.production.com',
  maxRetries: 5,
};

Recommended fix:

// src/config/app.ts
const config = {
  stripePublishableKey: process.env.NEXT_PUBLIC_STRIPE_KEY,
  apiUrl: process.env.NEXT_PUBLIC_API_URL,
  maxRetries: parseInt(process.env.MAX_RETRIES || '3'),
};

// .env.local (never committed)
// NEXT_PUBLIC_STRIPE_KEY=pk_live_...
// STRIPE_SECRET_KEY=sk_live_...  (server-side only)
// NEXT_PUBLIC_API_URL=https://api.production.com

Why this matters: A secret key in source code is permanently in git history, even if deleted later. Anyone with repository access (current and former employees, compromised CI) can extract it. For a Stripe secret key, this means full access to payment operations.

... and 5 more findings in the full report.

Recommendations

  1. Fix the WebSocket memory leak immediately. Add cleanup return functions to all useEffect hooks that create subscriptions, timers, or connections.
  2. Add ErrorBoundary components at the route level and around critical sections (checkout, payment). This prevents cascading failures.
  3. Rotate the exposed Stripe key and move all secrets to environment variables. Audit git history for other committed secrets.
  4. Break up the checkout component into smaller, testable components: CheckoutForm, OrderSummary, PaymentSection, ShippingForm.
  5. Standardize error handling with a shared useApiCall hook that handles loading, error, and success states consistently.

Want this level of detail for your code?

Get a comprehensive code review delivered within 24 hours.

Get Code Review

Security Audit Report

ScanMyCode.dev Security Audit (AUD-R4M7JW3NPQXZ)

February 10, 2026

Repository: acme-corp/api-backend

Review Date: February 10, 2026

Reviewer: ScanMyCode.dev Security Audit (AUD-R4M7JW3NPQXZ)

Tech Stack: Express.js, Node.js 20, PostgreSQL, JWT, bcrypt, Redis

Score

Overall Score: 38 / 100

CRITICAL: The API backend has multiple severe security vulnerabilities that make it unsuitable for production deployment. The most dangerous issues are unparameterized SQL queries allowing injection attacks, completely unprotected admin endpoints, and AWS credentials committed to source code. The application does use bcrypt for password hashing and has CSRF tokens on forms, but these positives are far outweighed by the critical gaps. The authentication system lacks rate limiting, allowing unlimited brute-force attempts against user accounts.

Strengths

  • Passwords hashed with bcrypt: Cost factor 12, which provides adequate protection against offline brute-force attacks.
  • CSRF tokens on forms: All form submissions include and validate CSRF tokens.
  • HTTPS enforced: Strict transport security headers are configured correctly.
  • Input length limits: Request body size limits are set to 10KB on most endpoints.

Findings Overview

#FindingSeverityLocation
1SQL Injection in user search endpointCriticalsrc/routes/users.ts:34
2Unauthenticated admin API endpointsCriticalsrc/routes/admin/*.ts
3AWS credentials hardcoded in sourceCriticalsrc/config/aws.ts:12-15
4No rate limiting on login endpointHighsrc/routes/auth.ts:45
5JWT secret is only 8 charactersHighsrc/lib/jwt.ts:3
6Missing Content-Security-Policy headerMediumsrc/middleware/headers.ts
7Session tokens do not expireMediumsrc/lib/session.ts:22

Detailed Findings

1. SQL Injection in user search endpoint

Severity: CriticalLocation: src/routes/users.ts:34

The user search endpoint directly interpolates the query parameter into a SQL string without parameterization. An attacker can inject arbitrary SQL, including UNION-based attacks to extract data from other tables, or destructive statements to modify/delete data.

Current code:

// src/routes/users.ts - Direct string interpolation in SQL
app.get('/api/users/search', async (req, res) => {
  const { q } = req.query;

  const users = await db.query(
    `SELECT * FROM users WHERE name LIKE '%${q}%'`
  );

  res.json(users);
});

// Attack: /api/users/search?q=' UNION SELECT username,password FROM users--

Recommended fix:

app.get('/api/users/search', async (req, res) => {
  const { q } = req.query;

  const users = await db.query(
    'SELECT * FROM users WHERE name LIKE $1',
    [`%${q}%`]
  );

  res.json(users);
});

Why this matters: SQL injection is consistently ranked #1 in the OWASP Top 10. With this vulnerability, an attacker can extract the entire database (user credentials, payment data, personal information), modify records, or delete tables. A single vulnerable endpoint compromises all data in the database.

2. Unauthenticated admin API endpoints

Severity: CriticalLocation: src/routes/admin/*.ts

Every admin endpoint is completely open. There is no authentication, session validation, or role-based access control. Anyone who discovers the endpoint URLs can view all user data, modify orders, and delete records.

Current code:

// src/routes/admin/users.ts - No auth check
export default async function handler(req, res) {
  const users = await db.query('SELECT * FROM users');
  const orders = await db.query('SELECT * FROM orders');

  res.json({ users, orders });
}

Recommended fix:

import { withAuth } from '@/lib/auth-middleware';

export default withAuth(async function handler(req, res) {
  if (req.user.role !== 'admin') {
    return res.status(403).json({ error: 'Forbidden' });
  }

  const users = await db.query('SELECT id, name, email FROM users');
  res.json({ users });
}, { role: 'admin' });

Why this matters: Without authentication, the admin endpoints are effectively public. An attacker or even an automated scanner can discover these endpoints and gain full read/write access to all application data, including personally identifiable information.

... and 5 more findings in the full report.

Recommendations

  1. URGENT: Fix the SQL injection vulnerability before any deployment. This is the single most dangerous finding - switch to parameterized queries or use an ORM.
  2. Add authentication middleware to all admin routes. Implement role-based access control and verify both authentication and authorization.
  3. Rotate all hardcoded credentials immediately. Assume any credential that has been in source code is compromised. Move to environment variables or a secrets manager.
  4. Implement rate limiting on the login endpoint. Maximum 5 attempts per minute per IP, with exponential backoff and account lockout after 15 failed attempts.
  5. Generate a strong JWT secret (minimum 256 bits / 32 bytes of cryptographic randomness) and store it as an environment variable.

Want this level of detail for your code?

Get a comprehensive security audit delivered within 24 hours.

Get Security Audit

Performance Audit Report

ScanMyCode.dev Performance Audit (AUD-V5N9GT2HKDWF)

February 8, 2026

Repository: acme-corp/dashboard-app

Review Date: February 8, 2026

Reviewer: ScanMyCode.dev Performance Audit (AUD-V5N9GT2HKDWF)

Tech Stack: Next.js 15, React 19, PostgreSQL, Prisma, Tailwind CSS 4

Score

Overall Score: 52 / 100

The dashboard application has significant performance bottlenecks that will become critical as the user base grows. The most impactful issue is a classic N+1 query pattern that generates 51 database queries where 1-2 would suffice, causing the dashboard API to take 3.2 seconds. Combined with missing database indexes on frequently queried foreign keys, the application will not scale beyond ~100 concurrent users without major refactoring. The frontend also loads full-resolution images without optimization, resulting in an 8MB initial page load.

Strengths

  • Server-side rendering: Pages are SSR'd, providing good initial load performance for content above the fold.
  • React.memo usage: Critical list components are wrapped in React.memo to prevent unnecessary re-renders.
  • Database connection pooling: Prisma is configured with appropriate connection pool limits for the expected load.

Findings Overview

#FindingSeverityLocation
1N+1 query pattern in dashboard data loadingCriticalsrc/lib/data.ts:23-35
2Missing database indexes on foreign keysCriticalDatabase schema
3Full-resolution images loaded without optimizationHighsrc/components/ProductGrid.tsx
4Bundle includes 800KB of unused lodash methodsMediumpackage.json
5No query result cachingMediumsrc/lib/data.ts

Detailed Findings

1. N+1 query pattern in dashboard data loading

Severity: CriticalLocation: src/lib/data.ts:23-35

getDashboardData() fetches all users in one query, then executes a separate query per user to fetch their orders. With 50 users displayed on the dashboard, this results in 51 database round-trips on every page load.

Current code:

// src/lib/data.ts - 51 database queries!
async function getDashboardData() {
  const users = await db.query('SELECT * FROM users LIMIT 50');

  const dashboardData = [];
  for (const user of users) {
    const orders = await db.query(
      'SELECT * FROM orders WHERE user_id = $1',
      [user.id]
    );
    dashboardData.push({ user, orders });
  }

  return dashboardData; // 1 + 50 = 51 queries
}

Recommended fix:

// Option 1: Single JOIN query
async function getDashboardData() {
  const data = await db.query(`
    SELECT u.*, json_agg(o.*) as orders
    FROM users u
    LEFT JOIN orders o ON u.id = o.user_id
    GROUP BY u.id
    LIMIT 50
  `);
  return data;
}

// Option 2: Prisma eager loading
const users = await prisma.user.findMany({
  take: 50,
  include: { orders: true }
});

Why this matters: This is a classic N+1 performance anti-pattern. The dashboard API response time is 3.2 seconds due to 51 sequential database round-trips. With a JOIN or eager loading, this drops to ~280ms (91% faster). As the user count grows, response times will grow linearly.

2. Missing database indexes on foreign keys

Severity: CriticalLocation: Database schema

The orders table has no index on user_id or product_id. Every query that filters by these columns results in a full table scan across all 100,000+ rows.

Current code:

-- No indexes on foreign keys
CREATE TABLE orders (
  id SERIAL PRIMARY KEY,
  user_id INTEGER NOT NULL,   -- No index!
  product_id INTEGER NOT NULL, -- No index!
  amount DECIMAL(10,2),
  created_at TIMESTAMP
);

-- This scans ALL 100,000 rows every time:
SELECT * FROM orders WHERE user_id = 123;
-- EXPLAIN: Seq Scan on orders (cost=0.00..15000.00)

Recommended fix:

-- Add indexes on frequently queried columns
CREATE INDEX idx_orders_user_id ON orders(user_id);
CREATE INDEX idx_orders_product_id ON orders(product_id);
CREATE INDEX idx_orders_created_at ON orders(created_at);

-- Composite index for common dashboard query
CREATE INDEX idx_orders_user_created
  ON orders(user_id, created_at DESC);

-- Now uses index scan:
SELECT * FROM orders WHERE user_id = 123;
-- EXPLAIN: Index Scan using idx_orders_user_id (cost=0.29..8.31)

Why this matters: Without indexes, every order lookup requires scanning the entire table. A query that should take 5ms takes 2-5 seconds. Combined with the N+1 pattern (Finding #1), the dashboard makes 50 full table scans per load.

... and 3 more findings in the full report.

Recommendations

  1. Fix the N+1 query pattern by using a single JOIN query or Prisma's include for eager loading. This alone will reduce dashboard load time from 3.2s to ~280ms.
  2. Add database indexes on all foreign key columns and frequently filtered columns. This is the highest-impact change per effort.
  3. Use `next/image` for all product images. It provides automatic resizing, lazy loading, and WebP conversion, reducing page weight from 8MB to ~200KB.
  4. Replace full lodash import with individual function imports (lodash/debounce instead of lodash) to eliminate 800KB of unused code from the bundle.
  5. Add query result caching with a 30-second TTL for dashboard data that doesn't need to be real-time.

Want this level of detail for your code?

Get a comprehensive performance audit delivered within 24 hours.

Get Performance Audit