vibeward
Sample report

What it actually prints

This is a complete report, unedited. It came from scanning a deliberately broken demo app served on localhost — not somebody's live site, which is why the address in it says so. Every competing scanner shows you a screenshot of a dashboard; this is the artefact you would hand a client.

Two things worth reading closely

The hardcoded bearer token is found by its use, not its shape. After minification the variable is a single letter and the value has no vendor prefix, so there is nothing to pattern-match: what gives it away is a literal that ends up behind Bearer. A token the client obtains at login is an identifier with no literal, and stays silent.

The Supabase publishable key three lines above it is reported as nothing at all. It belongs in the client by design, and a scanner that flags it is training you to ignore the scanner. Six of eight findings on a real corpus used to be exactly that kind of noise.

The website section is scored on its own scale. It never affects the security verdict, the SARIF file or the exit code — a missing favicon is not a finding next to a leaked credential.

Audit Report

Application: http://localhost:8099/

Date: 2026-08-16

Scope: Automated, non-destructive analysis of the public frontend and data API, plus website quality and AI visibility. Does not include manual penetration testing or server-side code review.


Executive summary

NEEDS URGENT ATTENTION before staying in production.
SeverityFindings
🔴 Critical0
🟠 High1
🟡 Medium2
⚪ Low4

Separately, 12 website quality issue(s) were found — see the section below. They do not affect the security verdict.

Detailed findings

1. 🟠 HIGH — Hardcoded bearer token in client code

Classification: CWE-798

Where: http://localhost:8099/assets/app.js

Evidence: Authorization: Bearer 7d3f1a9c…d2b6

How it's exploited: The key ships in the client JavaScript, so anyone can open the page source, copy it, and call the provider as you — no authentication needed. The token is in a file the site serves to everyone, so every visitor holds the same credential. Anyone who opens the network tab can replay it against the API routes it authorises.

Why it matters: A bearer token compiled into the bundle is a shared secret handed to every visitor, and it cannot be revoked for one person without revoking it for all. Whether that grants access to anything depends on what the server does with it — but a static token in client code is not authentication, and if it is the only check, the API is effectively public.

References: https://cwe.mitre.org/data/definitions/798.html · https://cwe.mitre.org/data/definitions/798.html

Checklist item 1.

2. 🟡 MEDIUM — Missing Content-Security-Policy header

Classification: CWE-693

Evidence: Absent from the response

How it's exploited: Without a CSP, an injected or third-party script can load and run from any origin, exfiltrating tokens or user data.

Why it matters: The main defense-in-depth against cross-site scripting is missing.

References: https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP · https://cwe.mitre.org/data/definitions/693.html

Checklist item 22.

3. 🟡 MEDIUM — Source map exposed — original source code is downloadable

Classification: CWE-540

Where: http://localhost:8099/assets/app.js.map

Evidence: http://localhost:8099/assets/app.js.map is served publicly and embeds the full original source (sourcesContent).

How it's exploited: Anyone opens http://localhost:8099/assets/app.js.map and reconstructs the unminified source — component names, comments, inline configuration and any hard-coded value the bundler left in.

Impact: The original source code is readable by anyone, which turns a black box into an open book: every other weakness becomes far easier to find, and secrets left in code are handed over directly.

Why it matters: Production builds should not ship source maps to the public. Disable source-map emission for prod, or strip the .map files from the deployed output.

References: https://cwe.mitre.org/data/definitions/540.html · https://webpack.js.org/configuration/devtool/

Checklist item 23.

4. ⚪ LOW — Missing Strict-Transport-Security (HSTS) header

Classification: CWE-319

Evidence: Absent from the response

How it's exploited: An on-path attacker can force a victim onto plain HTTP on the first request and intercept credentials or session cookies.

Why it matters: HTTPS is not pinned for future visits, so the browser is willing to try plain HTTP again next time. The header tells it never to, for a stated period — Strict-Transport-Security: max-age=63072000; includeSubDomains is the usual setting, and it belongs on the CDN or the server, not in the page.

References: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Strict-Transport-Security

Checklist item 22.

5. ⚪ LOW — Missing X-Frame-Options header

Classification: CWE-1021

Evidence: Absent from the response

How it's exploited: The app can be embedded in a hidden iframe on a malicious page to trick users into clicking actions (clickjacking).

Why it matters: No framing protection (also settable via CSP frame-ancestors).

References: https://cwe.mitre.org/data/definitions/1021.html

Checklist item 22.

6. ⚪ LOW — Missing X-Content-Type-Options header

Classification: CWE-430

Evidence: Absent from the response

How it's exploited: The browser may MIME-sniff a response as a different type than declared, enabling some script-execution attacks.

Why it matters: Without X-Content-Type-Options: nosniff the browser is allowed to second-guess the declared Content-Type and treat a file as whatever its bytes look like. An uploaded image that happens to parse as script is the classic case. It is one header with one value and no trade-offs.

References: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Content-Type-Options

Checklist item 22.

7. ⚪ LOW — server header reveals technology

Classification: CWE-200

Evidence: server: SimpleHTTP/0.6 Python/3.9.6

How it's exploited: Revealing the framework/server and its version lets an attacker look up known CVEs for that exact stack.

Why it matters: Information disclosure — hide the header at your host or framework.

References: https://cwe.mitre.org/data/definitions/200.html

Checklist item 23.


Website quality & AI visibility

These findings do not affect the security verdict or the exit code. They cover how this site is read by search engines, social platforms and AI assistants.

Vibe-coded fingerprint: 9/12 — HTML carries no content — JavaScript renders everything · No meta description · No Open Graph tags · No canonical URL · No structured data · No sitemap.xml · No favicon · No lang on <html> · No llms.txt

5 of 17 checks passed.

CheckStatusImpactNote
robots.txt lets AI crawlers in✅ ok
AI crawlers blocked as declared⚪ not applicableno declared intent to block AI crawlers
HTML carries real content🔴 High impact
No broken assets🔴 High impact
No JavaScript errors on load⚪ not evaluatedno browser available
Each page has its own <title>🔴 High impact
<title> present✅ ok
Meta description🟡 Medium impact
Open Graph tags🟡 Medium impact
Canonical URL🟡 Medium impact
Structured data (JSON-LD)🟡 Medium impact
Exactly one <h1> per page✅ ok
sitemap.xml🟡 Medium impact
llms.txt🟡 Medium impact
lang attribute on <html>🟡 Medium impact
Images have alt text🟡 Medium impact
Unknown URLs return 404✅ ok
Favicon⚪ Low impact
JavaScript under 1 MB✅ ok

Website findings in detail

1. 🔴 High impact — The HTML ships with no content

Evidence: The home page returns 90 characters of visible text before JavaScript runs (2 of 2 pages below 200 characters).

Impact: To anything that does not run JavaScript the site is a blank page: no headline, no copy, nothing to index or quote. Most AI crawlers never execute scripts, so the site simply does not exist for them.

Why it matters: A client-rendered shell hands crawlers an empty <div>. Google renders eventually and inconsistently; GPTBot, ClaudeBot and PerplexityBot do not render at all. Closing it is an architecture decision, not a text edit: either the real headline and first paragraph live inside the mount node in index.html so they are in the source before any script runs, or the pages are built by a renderer that emits HTML (Astro, Next.js server components, Vite SSG).

Pages: http://localhost:8099/, http://localhost:8099/app.html

2. 🔴 High impact — Assets referenced by the site return an error

Evidence: 1 referenced file answers with 4xx/5xx — http://localhost:8099/logo.png → 404 (referenced from http://localhost:8099/)

Impact: Visitors see missing images, unstyled sections or a feature that never loads — the fastest way to look abandoned and lose the sale on the first screen.

Why it matters: The page points at files that are not deployed: a path that only exists locally, a renamed asset, or an image the build never copied. Assets served from the site root have to live in public/ (Vite, Next.js, Astro) or static/ (SvelteKit) for the build to copy them — otherwise the reference has to go.

3. 🔴 High impact — Every page has the same <title>

Evidence: All 2 crawled pages send the same title: "Nimbus Invoices".

Impact: Every page competes for the same search result and none of them wins. The title is the blue line people click in Google and the label of every shared tab and bookmark.

Why it matters: Search engines treat identical titles as duplicate pages and pick one, dropping the rest. A one-page-app router that never updates the title is the usual cause. Each page needs its own <title> of roughly 50-60 characters, page subject first and brand last ("Pricing — Acme"); in a React SPA with no meta framework that means setting document.title per route.

Pages: http://localhost:8099/, http://localhost:8099/app.html

4. 🟡 Medium impact — Pages have no meta description

Evidence: 2 of 2 pages have no <meta name="description">.

Impact: Google fills the two lines under the link with whatever text it scrapes off the page, so the pitch a visitor reads before deciding to click is written by an algorithm instead of by the owner.

Why it matters: The description is the ad copy of the search result. It does not change ranking, it changes the click-through rate. One per page, 150-160 characters, written for a human deciding whether to click — a generic one is worse than none, because it looks written and so nobody ever writes the real one.

Pages: http://localhost:8099/, http://localhost:8099/app.html

5. 🟡 Medium impact — Pages have no Open Graph tags

Evidence: 2 of 2 pages are missing og:title or og:image.

Impact: Shared on WhatsApp, LinkedIn, Slack or X the link renders as a naked URL with no image and no title, which people scroll past. Every share the site gets is wasted.

Why it matters: Open Graph tags are what social platforms and chat apps read to build the preview card. Without them there is no card. A complete set in each page's <head> is og:title, og:description, og:image, og:url and og:type, plus twitter:card set to summary_large_image — and og:image has to be a real 1200x630 image, since a missing one renders as a blank box rather than no card at all.

Pages: http://localhost:8099/, http://localhost:8099/app.html

References: https://ogp.me/

6. 🟡 Medium impact — Pages declare no canonical URL

Evidence: 2 of 2 pages have no <link rel="canonical">.

Impact: The same page reachable at www and non-www, with and without a trailing slash, or with a campaign parameter, is counted as several competing pages, splitting whatever authority the site has earned.

Why it matters: The canonical tag names the one real address of a page so duplicates consolidate into it instead of competing with it. Each page needs its own <link rel="canonical"> pointing at its own absolute URL (http://localhost:8099/pricing on the pricing page) — pointing every page at the home page is the common mistake, and it tells search engines the rest of the site does not exist.

Pages: http://localhost:8099/, http://localhost:8099/app.html

References: https://developers.google.com/search/docs/crawling-indexing/consolidate-duplicate-urls

7. 🟡 Medium impact — No structured data (JSON-LD)

Evidence: 2 of 2 pages contain no <script type="application/ld+json"> block.

Impact: Search engines and assistants have to guess what the business is, what it sells and where it is. Rich results (ratings, prices, FAQ, opening hours) are not available to a site that never declares them.

Why it matters: Structured data is the machine-readable version of the page. It is how an assistant answers "who are they and what do they do" with facts instead of guesses. The minimum is one application/ld+json block in the home page <head> declaring an Organization with the real name, url, logo, description and sameAs profile links — the values have to be true, because this is the copy an assistant repeats verbatim.

Pages: http://localhost:8099/, http://localhost:8099/app.html

References: https://schema.org/Organization

8. 🟡 Medium impact — No sitemap.xml

Evidence: http://localhost:8099/sitemap.xml is not served.

Impact: Pages that are not linked from the home page can go unnoticed for weeks or never get indexed at all, and every update takes longer to show up in search.

Why it matters: A sitemap is the list of pages the owner wants indexed. Without it crawlers only find what they happen to stumble on. It is a urlset of <loc> entries at public/sitemap.xml (static/sitemap.xml on SvelteKit), with a Sitemap: http://localhost:8099/sitemap.xml line in robots.txt pointing at it — this scan reached 2 pages, listed under "Pages scanned".

References: https://www.sitemaps.org/protocol.html

9. 🟡 Medium impact — No llms.txt

Evidence: http://localhost:8099/llms.txt is not served.

Impact: When an assistant is asked about this business it has to reconstruct the offer from whatever markup it can parse. A short curated file is the difference between being described accurately and being described wrong.

Why it matters: llms.txt is the plain-text summary an AI reads first: what the site is, who it is for, and which pages matter. It lives at public/llms.txt — an H1 with the name, a blockquote with one sentence on the offer and the audience, and a ## Pages list of the pages worth reading with a line each. It only works if it says what the business actually sells; a placeholder version is worse than none, because it reads as authoritative.

References: https://llmstxt.org/

10. 🟡 Medium impact — The <html> tag declares no language

Evidence: 2 of 2 pages have no lang attribute on <html>.

Impact: Browsers offer to translate a page that is already in the language of the visitor, screen readers pick the wrong voice, and search engines can serve the site to the wrong country.

Why it matters: One attribute — lang on <html> — tells every client what language the content is written in. Templates ship with it empty or copied from the starter, so it has to be set to the language the copy is actually in, not the one the template came with.

Pages: http://localhost:8099/, http://localhost:8099/app.html

11. 🟡 Medium impact — Images have no alt text

Evidence: 1 of 1 images have no alt attribute, across 1 of 2 pages.

Impact: Blind visitors hear "image" instead of the product, image search never returns these pictures, and in several jurisdictions it is an accessibility obligation rather than a nicety.

Why it matters: Alt text is what an image says to anyone or anything that cannot see it: screen readers, image search, and any crawler indexing the page. It has to describe what the picture actually shows, in the page's language — which means looking at each image, not generating text from the filename. Purely decorative images take an empty alt="", which is different from having none.

Pages: http://localhost:8099/

12. ⚪ Low impact — No favicon

Evidence: No favicon is served and no <link rel="icon"> is declared on any crawled page.

Impact: The tab, the bookmark and the phone home screen show a blank sheet of paper. It is small, it is free to fix, and it is the difference between a business and a weekend demo.

Why it matters: The favicon is the only branding a site gets in a row of thirty open tabs. It needs a public/favicon.svg plus a 180x180 public/apple-touch-icon.png for phone home screens, both referenced with <link rel="icon"> and <link rel="apple-touch-icon"> in the <head>.


Coverage

This automated analysis covers what is verifiable from the outside. Items that require server access (authorization, input validation, rate limiting, backups) are covered by the manual part of the audit.

Verified automatically

Browser console errors were not inspected: Playwright is not installed. Install it (npm i -D playwright && npx playwright install chromium) and re-run to include runtime errors.

Recommended next steps

1. Address findings in order of severity. 2. Complement with a manual server-side review.

For the website section, npx vibeward@latest init installs a skill that reads these findings, applies the fixes it can, and re-scans to verify.


Generated with vibeward v0.6.0 — an automated, non-destructive analysis performed from the outside.

The app that produces it

Four files, broken on purpose. Nothing here is a real credential: the Supabase key is a syntactically valid value that points nowhere, and the bearer token is written as a placeholder — put any 48 hex characters there and you get the report above. The placeholder is not squeamishness, it is the tool working. With the real value printed here, this very page scanned as a hardcoded bearer token in client code, because a literal that reaches an Authorization header is exactly what the rule looks for and it cannot tell a demo from a deployment. A placeholder is not credential-shaped, so it stays silent — and security findings cannot be suppressed by config, by design.

python3 -m http.server 8099
npx vibeward@latest http://localhost:8099/ --yes

index.html

<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>Nimbus Invoices</title>
</head>
<body>
<h1>Nimbus Invoices</h1>
<p>Send invoices in seconds. Built in a weekend.</p>
<a href="app.html">Open the app</a>
<img src="logo.png">
<script src="assets/app.js"></script>
</body>
</html>

assets/app.js

//# sourceMappingURL=app.js.map
var SUPABASE_URL = "https://abcdefghijklmnop.supabase.co";
var SUPABASE_ANON_KEY = "sb_publishable_9f2c1d4e7a8b3c5d6e0f1a2b";
var B = "PUT_ANY_48_HEX_CHARACTERS_HERE";
function api(p) {
  return fetch(SUPABASE_URL + p, {
    headers: { apikey: SUPABASE_ANON_KEY, Authorization: "Bearer ".concat(B) }
  });
}
window.api = api;