Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions .github/workflows/linter.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,18 @@ jobs:
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.11'
- name: Install Python dependencies
run: |
python -m venv venv
. ./venv/bin/activate
pip install --upgrade pip setuptools
pip install -r requirements.txt
- name: OpenAPI guardrail
run: make openapi-guardrail
- name: Lint Code Base
uses: github/super-linter@v6
env:
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ standards_cache.sqlite
### Docs
*.md
!AGENTS.md
!docs/faq.md

### Dev DBDumps
*.sql
Expand Down
10 changes: 9 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ docker-prod-run:
docker run -it -p $(PORT):$(PORT) opencre:$(shell git rev-parse HEAD)

lint:
[ -d "./venv" ] && . ./venv/bin/activate && black . && yarn lint
[ -d "./venv" ] && . ./venv/bin/activate && black . && yarn lint && make openapi-guardrail

mypy:
[ -d "./venv" ] && . ./venv/bin/activate && mypy --ignore-missing-imports --implicit-reexport --no-strict-optional --strict application
Expand All @@ -122,6 +122,14 @@ alembic-guardrail:
[ -d "./venv" ] && . ./venv/bin/activate &&\
python scripts/check_alembic_revision_guardrail.py

openapi-generate:
[ -d "./venv" ] && . ./venv/bin/activate &&\
python scripts/generate_openapi.py

openapi-guardrail:
[ -d "./venv" ] && . ./venv/bin/activate &&\
python scripts/check_openapi_guardrail.py

migrate-downgrade:
[ -d "./venv" ] && . ./venv/bin/activate &&\
export FLASK_APP="$(CURDIR)/cre.py"
Expand Down
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,12 @@ OpenCRE consists of:
To see how you can contribute to the application or to the data (catalog or standard mappings), see [Contributing](docs/CONTRIBUTING.md).
We really welcome you!

# Documentation

- [In-app Docs](https://www.opencre.org/docs) — getting started, FAQ, and interactive API reference
- [FAQ](docs/faq.md) — frequently asked questions (also rendered at [/docs#faq](https://www.opencre.org/docs#faq))
- [OpenAPI spec](docs/api/openapi.yaml) — public REST API (`/rest/v1/openapi.yaml` when running)

# Roadmap

For a roadmap please see the [issues](https://github.com/OWASP/OpenCRE/issues).
Expand Down
4 changes: 4 additions & 0 deletions application/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,10 @@ def create_app(mode: str = "production", conf: any = None) -> Any:

app.register_blueprint(app_blueprint)

from application.web.openapi_registry import init_openapi

init_openapi(app)

CORS(app)
app.config["CORS_HEADERS"] = "Content-Type"

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import './markdownFromRepo.scss';

import DOMPurify from 'dompurify';
import { marked } from 'marked';
import React, { useEffect, useState } from 'react';

interface MarkdownFromRepoProps {
src: string;
className?: string;
errorHref?: string;
}

const FETCH_TIMEOUT_MS = 10_000;

const githubSourceHref = (src: string, errorHref?: string): string => {
if (errorHref) {
return errorHref;
}
const repoPath = src.replace(/^\//, '');
return `https://github.com/OWASP/OpenCRE/blob/main/${repoPath}`;
};

export const MarkdownFromRepo = ({ src, className = '', errorHref }: MarkdownFromRepoProps) => {
const [html, setHtml] = useState<string>('');
const [error, setError] = useState<string | null>(null);
const [loading, setLoading] = useState(true);

useEffect(() => {
const controller = new AbortController();
const timeout = window.setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);

setLoading(true);
setError(null);

fetch(src, { signal: controller.signal })
.then((res) => {
if (!res.ok) {
throw new Error(`Failed to load markdown (${res.status})`);
}
return res.text();
})
.then((markdown) => {
const parsed = marked.parse(markdown, { async: false });
const rendered = DOMPurify.sanitize(String(parsed), {
USE_PROFILES: { html: true },
});
setHtml(rendered);
})
.catch((err: Error) => {
if (err.name !== 'AbortError') {
setError(err.message);
}
})
.finally(() => {
window.clearTimeout(timeout);
if (!controller.signal.aborted) {
setLoading(false);
}
});

return () => {
controller.abort();
window.clearTimeout(timeout);
};
}, [src]);
Comment on lines +28 to +65

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Timeout aborts leave the component stuck on “Loading…” forever.

When the 10s timeout fires (Line 30), controller.abort() rejects the fetch with an AbortError. The catch skips setError because err.name === 'AbortError' (Line 50), and finally skips setLoading(false) because controller.signal.aborted is true (Line 56). The net effect on a slow/failed network is a permanent “Loading…” spinner with no error and no GitHub fallback link.

The signal.aborted guard is trying to suppress state updates after unmount, but it can't distinguish an unmount-abort from a timeout-abort. Use an explicit active flag set to false only in cleanup, and surface a timeout error to the user.

🐛 Proposed fix
   useEffect(() => {
     const controller = new AbortController();
+    let active = true;
     const timeout = window.setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);

     setLoading(true);
     setError(null);

     fetch(src, { signal: controller.signal })
       .then((res) => {
         if (!res.ok) {
           throw new Error(`Failed to load markdown (${res.status})`);
         }
         return res.text();
       })
       .then((markdown) => {
         const parsed = marked.parse(markdown, { async: false });
         const rendered = DOMPurify.sanitize(String(parsed), {
           USE_PROFILES: { html: true },
         });
-        setHtml(rendered);
+        if (active) {
+          setHtml(rendered);
+        }
       })
       .catch((err: Error) => {
-        if (err.name !== 'AbortError') {
-          setError(err.message);
-        }
+        if (!active) {
+          return;
+        }
+        setError(err.name === 'AbortError' ? 'Request timed out' : err.message);
       })
       .finally(() => {
         window.clearTimeout(timeout);
-        if (!controller.signal.aborted) {
+        if (active) {
           setLoading(false);
         }
       });

     return () => {
+      active = false;
       controller.abort();
       window.clearTimeout(timeout);
     };
   }, [src]);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
useEffect(() => {
const controller = new AbortController();
const timeout = window.setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
setLoading(true);
setError(null);
fetch(src, { signal: controller.signal })
.then((res) => {
if (!res.ok) {
throw new Error(`Failed to load markdown (${res.status})`);
}
return res.text();
})
.then((markdown) => {
const parsed = marked.parse(markdown, { async: false });
const rendered = DOMPurify.sanitize(String(parsed), {
USE_PROFILES: { html: true },
});
setHtml(rendered);
})
.catch((err: Error) => {
if (err.name !== 'AbortError') {
setError(err.message);
}
})
.finally(() => {
window.clearTimeout(timeout);
if (!controller.signal.aborted) {
setLoading(false);
}
});
return () => {
controller.abort();
window.clearTimeout(timeout);
};
}, [src]);
useEffect(() => {
const controller = new AbortController();
let active = true;
const timeout = window.setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
setLoading(true);
setError(null);
fetch(src, { signal: controller.signal })
.then((res) => {
if (!res.ok) {
throw new Error(`Failed to load markdown (${res.status})`);
}
return res.text();
})
.then((markdown) => {
const parsed = marked.parse(markdown, { async: false });
const rendered = DOMPurify.sanitize(String(parsed), {
USE_PROFILES: { html: true },
});
if (active) {
setHtml(rendered);
}
})
.catch((err: Error) => {
if (!active) {
return;
}
setError(err.name === 'AbortError' ? 'Request timed out' : err.message);
})
.finally(() => {
window.clearTimeout(timeout);
if (active) {
setLoading(false);
}
});
return () => {
active = false;
controller.abort();
window.clearTimeout(timeout);
};
}, [src]);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@application/frontend/src/components/MarkdownFromRepo/MarkdownFromRepo.tsx`
around lines 28 - 65, The MarkdownFromRepo useEffect in MarkdownFromRepo.tsx
leaves the UI stuck in loading state when FETCH_TIMEOUT_MS aborts the request
because the AbortError path skips both setError and setLoading(false). Update
the fetch flow to distinguish a timeout abort from cleanup/unmount by using an
explicit active flag in the effect cleanup, and when the timeout controller
triggers, surface a user-visible timeout error instead of suppressing it. Keep
the existing fetch/marked.parse/DOMPurify logic, but ensure setLoading(false)
runs for timeout failures while still preventing state updates after unmount.


if (loading) {
return <p className="markdown-from-repo__status">Loading…</p>;
}

if (error) {
return (
<div className="markdown-from-repo__error">
<p>{error}</p>
<p>
View the source on{' '}
<a href={githubSourceHref(src, errorHref)} target="_blank" rel="noreferrer">
GitHub
</a>
.
</p>
</div>
);
}

return (
<div className={`markdown-from-repo ${className}`.trim()} dangerouslySetInnerHTML={{ __html: html }} />
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
.markdown-from-repo {
color: var(--muted-foreground, #cbd5e1);
line-height: 1.7;
font-size: 1.05rem;

h2 {
color: #fff;
font-size: 1.35rem;
margin: 2rem 0 0.75rem;
}

h2:first-child {
margin-top: 0;
}

p {
margin: 0 0 1rem;
}

a {
color: #93c5fd;
text-decoration: none;

&:hover {
color: #bfdbfe;
text-decoration: underline;
}
}

ul,
ol {
margin: 0 0 1rem 1.25rem;
}

code {
background: rgba(148, 163, 184, 0.15);
padding: 0.1rem 0.35rem;
border-radius: 0.25rem;
font-size: 0.95em;
}
}

.markdown-from-repo__status,
.markdown-from-repo__error {
color: var(--muted-foreground, #cbd5e1);
}

.markdown-from-repo__error a {
color: #93c5fd;
}
1 change: 1 addition & 0 deletions application/frontend/src/const.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,5 +38,6 @@ export const GRAPH = '/graph';
export const BROWSEROOT = '/root_cres';
export const GAP_ANALYSIS = '/map_analysis';
export const EXPLORER = '/explorer';
export const DOCS = '/docs';

export const GA_STRONG_UPPER_LIMIT = 2; // remember to change this in the Python code too
118 changes: 118 additions & 0 deletions application/frontend/src/pages/Docs/Docs.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
import 'swagger-ui-react/swagger-ui.css';

import './docs.scss';

import React, { useEffect } from 'react';
import SwaggerUI from 'swagger-ui-react';

import { MarkdownFromRepo } from '../../components/MarkdownFromRepo/MarkdownFromRepo';
import { useEnvironment } from '../../hooks';

const scrollToHash = () => {
const hash = window.location.hash.replace('#', '');
if (!hash) return;
const el = document.getElementById(hash);
if (el) {
el.scrollIntoView({ behavior: 'smooth' });
}
};

export const Docs = () => {
const { apiUrl } = useEnvironment();

useEffect(() => {
window.scrollTo(0, 0);
scrollToHash();
}, []);

useEffect(() => {
window.addEventListener('hashchange', scrollToHash);
return () => window.removeEventListener('hashchange', scrollToHash);
}, []);

return (
<div className="docs-page">
<header className="docs-page__hero">
<h1 className="docs-page__title">Documentation</h1>
<p className="docs-page__subtitle">
Getting started with OpenCRE, the public REST API, and frequently asked questions.
</p>
<nav className="docs-page__nav" aria-label="Docs sections">
<a href="#getting-started">Getting started</a>
<a href="#api-reference">API reference</a>
<a href="#faq">FAQ</a>
<a href="#resources">Resources</a>
</nav>
</header>

<section id="getting-started" className="docs-section">
<h2 className="docs-section__title">Getting started</h2>
<div className="docs-section__body">
<p>
OpenCRE unifies security standards and guidelines through Common Requirements (CREs). Use the
homepage search, <a href="/root_cres">Browse</a>, <a href="/map_analysis">Map Analysis</a>, or the{' '}
<a href="/chatbot">Chat</a> assistant to explore mapped content.
</p>
<p>
Integrators can use the read-only REST API under <code>{apiUrl}</code>. The OpenAPI specification
is generated from the backend route registry and validated in CI.
</p>
</div>
</section>

<section id="api-reference" className="docs-section">
<h2 className="docs-section__title">API reference</h2>
<p className="docs-section__intro">
Interactive documentation for the public API. Raw spec:{' '}
<a href={`${apiUrl}/openapi.yaml`} target="_blank" rel="noreferrer">
{apiUrl}/openapi.yaml
</a>
</p>
<div className="docs-page__swagger">
<SwaggerUI url={`${apiUrl}/openapi.yaml`} docExpansion="list" defaultModelsExpandDepth={0} />
</div>
</section>

<section id="faq" className="docs-section">
<h2 className="docs-section__title">FAQ</h2>
<MarkdownFromRepo src="/docs/faq.md" />
</section>

<section id="resources" className="docs-section">
<h2 className="docs-section__title">Resources</h2>
<ul className="docs-resources">
<li>
<a href="https://github.com/OWASP/OpenCRE/blob/main/README.md" target="_blank" rel="noreferrer">
README
</a>
</li>
<li>
<a
href="https://github.com/OWASP/OpenCRE/blob/main/docs/CONTRIBUTING.md"
target="_blank"
rel="noreferrer"
>
Contributing
</a>
</li>
<li>
<a
href="https://github.com/OWASP/OpenCRE/blob/main/docs/my-opencre-user-guide.md"
target="_blank"
rel="noreferrer"
>
MyOpenCRE user guide
</a>
</li>
<li>
<a href="https://github.com/OWASP/OpenCRE" target="_blank" rel="noreferrer">
GitHub repository
</a>
</li>
</ul>
</section>
</div>
);
};

export default Docs;
Loading
Loading