Skip to content

Commit

Permalink
Feature/deployment (#8)
Browse files Browse the repository at this point in the history
* Respect API URL or use same host as FE

* Add Docker deployment option

* Fix issues with production build

* Add sample .env file

* Add comment to .env file

---------

Co-authored-by: Matěj Groman <[email protected]>
  • Loading branch information
Matej4545 and Matěj Groman authored Jan 30, 2023
1 parent a0fe80a commit 9c3ce6b
Show file tree
Hide file tree
Showing 20 changed files with 228 additions and 187 deletions.
23 changes: 11 additions & 12 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,21 +22,20 @@ services:
redis-cache:
image: redis:alpine
restart: unless-stopped
ports:
ports:
- 6379:6379
command: redis-server --save 20 1 --loglevel notice
volumes:
volumes:
- cache:/data
depvis-next:
depends_on:
- neo4j
- redis-cache
build: ./src/depvis-next
image: depvis-next:latest
ports:
- 80:3000
restart: always
volumes:
cache:
driver: local

# depvis-next:
# depends_on:
# - neo4j
# build: ./src/depvis-next
# image: depvis-next:latest
# ports:
# - 80:80
# - 443:443
# restart: always
7 changes: 7 additions & 0 deletions src/depvis-next/.dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
Dockerfile
.dockerignore
node_modules
npm-debug.log
README.md
.next
.git
13 changes: 13 additions & 0 deletions src/depvis-next/.env.production
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Sample .env file that can be used for deployment via docker
# Do not forget to change passwords to correspond with docker-compose file
NEO4J_USER=neo4j
NEO4J_PASSWORD=<PASSWORD>
NEO4J_HOST=neo4j://neo4j:7687

NEXT_PUBLIC_SONATYPE_OSS_AUTH=<TOKEN>

GQL_ALLOW_DEV_TOOLS=true

REDIS_HOST=redis-cache
REDIS_PORT=6379
REDIS_PASSWORD=<PASSWORD>
5 changes: 4 additions & 1 deletion src/depvis-next/.eslintrc.json
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
{
"extends": "next/core-web-vitals"
"extends": "next/core-web-vitals",
"rules": {
"react-hooks/exhaustive-deps": "off"
}
}
56 changes: 56 additions & 0 deletions src/depvis-next/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
# Dockerfile created from example at https://github.com/vercel/next.js/blob/canary/examples/with-docker/Dockerfile
FROM node:alpine AS deps
# Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed.
RUN apk add --no-cache libc6-compat
WORKDIR /app

# Install dependencies based on the preferred package manager
COPY package.json yarn.lock* package-lock.json* pnpm-lock.yaml* ./
RUN \
if [ -f yarn.lock ]; then yarn --frozen-lockfile; \
elif [ -f package-lock.json ]; then npm ci; \
elif [ -f pnpm-lock.yaml ]; then yarn global add pnpm && pnpm i --frozen-lockfile; \
else echo "Lockfile not found." && exit 1; \
fi


# Rebuild the source code only when needed
FROM node:alpine AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .

# Next.js collects completely anonymous telemetry data about general usage.
# Learn more here: https://nextjs.org/telemetry
# Uncomment the following line in case you want to disable telemetry during the build.
# ENV NEXT_TELEMETRY_DISABLED 1

RUN npm run build

# Production image, copy all the files and run next
FROM node:alpine AS runner
WORKDIR /app

ENV NODE_ENV production
# Uncomment the following line in case you want to disable telemetry during runtime.
# ENV NEXT_TELEMETRY_DISABLED 1

RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs

COPY --from=builder /app/public ./public

# Automatically leverage output traces to reduce image size
# https://nextjs.org/docs/advanced-features/output-file-tracing
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
# Copy .env production variables
COPY .env.production .

USER nextjs

EXPOSE 3000

ENV PORT 3000

CMD ["node", "server.js"]
17 changes: 16 additions & 1 deletion src/depvis-next/README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,21 @@
# Depvis-next

This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app).

## Getting Started
A language-independent package dependencies visualization tool that use [CycloneDX](https://cyclonedx.org/) SBOM formatted files as input and creates graphic visualization that can help identify vulnerabilities and software structure.

## Deployment

### Docker (preferred way)

- This application can be deployed as Docker container - see [Dockerfile](./Dockerfile) for more details.
- During deployment, a file `.env.production` is used to set secrets and other variables. Make sure to provide at least connection strings to neo4j DB and redis cache. Values should be corresponding to the one set in `docker-compose.yaml` if used to run whole infrastructure.

### Other

- For other types of deployment please follow instructions in [Next.js documentation](https://nextjs.org/docs/deployment)

## Development

- create `.env.local` file in this directory and set variables (see dotenv.sample for available variables)
- provide Neo4J connection variables for proper functionality
Expand Down
29 changes: 11 additions & 18 deletions src/depvis-next/components/Details/ComponentDetails.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,12 @@
import { gql, useQuery } from "@apollo/client";
import { Container } from "react-bootstrap";
import { GetComponentRepositoryURL } from "../../helpers/WorkspaceHelper";
import Loading from "../Loading/Loading";
import { DL, DLItem } from "./DescriptionList";
import { gql, useQuery } from '@apollo/client';
import { Container } from 'react-bootstrap';
import { GetComponentRepositoryURL } from '../../helpers/WorkspaceHelper';
import Loading from '../Loading/Loading';
import { DL, DLItem } from './DescriptionList';

const getComponentDetailsQuery = gql`
query componentDetails($componentPurl: String, $projectId: ID) {
components(
where: { purl: $componentPurl, project_SINGLE: { id: $projectId } }
) {
components(where: { purl: $componentPurl, project_SINGLE: { id: $projectId } }) {
name
purl
author
Expand All @@ -30,22 +28,20 @@ const ComponentDetails = (props) => {
const renderLink = () => {
const link = GetComponentRepositoryURL(data.components[0].purl);
return (
<a href={link} target="_blank">
<a href={link} target="_blank" rel="noreferrer">
{link}
</a>
);
};
if (loading) return <Loading />;
if (!data.components[0]) {
console.error(
"No data found when querying backend! Below is Apollo query result"
);
console.error('No data found when querying backend! Below is Apollo query result');
console.error({ data: data, error: error });
return <b>No data found!</b>;
}
const component = data.components[0];
return (
<Container style={{ wordBreak: "break-all" }} className="px-0">
<Container style={{ wordBreak: 'break-all' }} className="px-0">
<h4 className="pb-3">
<b>{component.name}</b>
</h4>
Expand All @@ -54,14 +50,11 @@ const ComponentDetails = (props) => {
<DLItem label="Author" value={component.author} />
<DLItem label="Publisher" value={component.publisher} />
<DLItem label="Purl" value={component.purl} />
<DLItem
label="Number of dependencies"
value={component.dependsOnCount}
/>
<DLItem label="Number of dependencies" value={component.dependsOnCount} />
<DLItem
label="Vulnerabilities"
value={component.vulnerabilities.map((v) => (
<p>{v.id}</p>
<p key={v.id}>{v.id}</p>
))}
/>
<DLItem label="External resources" value={renderLink()} />
Expand Down
2 changes: 1 addition & 1 deletion src/depvis-next/components/Details/Details.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ export default function Details(props) {
<tbody>
{props.data &&
Object.entries(props.data).map(([key, value]) => (
<tr>
<tr key={key}>
<td style={{ wordBreak: 'break-all' }}>
<b>{key}</b>
</td>
Expand Down
36 changes: 12 additions & 24 deletions src/depvis-next/components/Details/VulnerabilityDetails.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { gql, useQuery } from "@apollo/client";
import { Badge, Container } from "react-bootstrap";
import Loading from "../Loading/Loading";
import { DL, DLItem } from "./DescriptionList";
import { gql, useQuery } from '@apollo/client';
import { Badge, Container } from 'react-bootstrap';
import Loading from '../Loading/Loading';
import { DL, DLItem } from './DescriptionList';

const getVulnerabilityDetailsQuery = gql`
query vulnerabilityDetails($vulnerabilityId: String) {
Expand Down Expand Up @@ -30,51 +30,39 @@ const VulnerabilityDetails = (props) => {
const renderReferences = (referencesList) => {
if (!referencesList) return;
return referencesList.map((r, index) => (
<li className="overflow-text">
<a href={r.url} target="_blank">
<li className="overflow-text" key={index}>
<a href={r.url} target="_blank" rel="noreferrer">
{r.url}
</a>
</li>
));
};

const renderCvss = (cvssScore) => {
return <Badge bg={cvssScore > 5 ? "danger" : "warning"}>{cvssScore}</Badge>;
return <Badge bg={cvssScore > 5 ? 'danger' : 'warning'}>{cvssScore}</Badge>;
};

if (loading) return <Loading />;
if (!data.vulnerabilities[0]) {
console.error(
"No data found when querying backend! Below is Apollo query result"
);
console.error('No data found when querying backend! Below is Apollo query result');
console.error({ data: data, error: error });
return <b>No data found!</b>;
}
const vulnerability = data.vulnerabilities[0];
return (
<Container style={{ wordBreak: "break-word" }} className="px-0">
<Container style={{ wordBreak: 'break-word' }} className="px-0">
<h4 className="pb-3">
<b>{vulnerability.name}</b>
</h4>
<DL>
<DLItem label="Id" value={vulnerability.id} />
<DLItem
label="CVSS Score"
value={renderCvss(vulnerability.cvssScore)}
alwaysShow
/>
<DLItem label="CVSS Score" value={renderCvss(vulnerability.cvssScore)} alwaysShow />
<DLItem label="CVSS Vector" value={vulnerability.cvssVector} />
<DLItem label="Description" value={vulnerability.description} />

<DLItem
label="affectedVersions"
value={vulnerability.affectedVersions}
/>
<DLItem label="affectedVersions" value={vulnerability.affectedVersions} />
<DLItem label="CWE" value={vulnerability.cwe} />
<DLItem
label="References"
value={renderReferences(vulnerability.references)}
/>
<DLItem label="References" value={renderReferences(vulnerability.references)} />
</DL>
</Container>
);
Expand Down
16 changes: 5 additions & 11 deletions src/depvis-next/components/Error/GenericError.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,14 @@
import { useRouter } from "next/router";
import { Button, Container } from "react-bootstrap";
import Link from 'next/link';
import { useRouter } from 'next/router';
import { Container } from 'react-bootstrap';

export default function GenericError(props) {
const router = useRouter();
return (
<Container className="mx-auto my-5 text-center">
<p className="fs-4 fw-bold">{props.error.message || "Error occured!"}</p>
<p className="fs-4 fw-bold">{props.error.message || 'Error occured!'}</p>
<pre>{JSON.stringify(props.error)}</pre>
<Button
onClick={() => {
router.reload(window.location.pathname);
}}
variant="primary"
>
Refresh page
</Button>
<Link href="/">Go to homepage</Link>
</Container>
);
}
20 changes: 9 additions & 11 deletions src/depvis-next/components/GraphControl/GraphControl.tsx
Original file line number Diff line number Diff line change
@@ -1,21 +1,19 @@
import { useEffect, useState } from "react";
import { Button, Container, Form, Stack } from "react-bootstrap";
import { getNodeValue } from "../../helpers/GraphHelper";
import { GraphConfig } from "../Graph/GraphConfig";
import { useEffect, useState } from 'react';
import { Button, Container, Form, Stack } from 'react-bootstrap';
import { getNodeValue } from '../../helpers/GraphHelper';
import { GraphConfig } from '../Graph/GraphConfig';

const GraphControl = (props) => {
const { defaultGraphConfig, onGraphConfigChange, onRefetchGraphClick } =
props;
const { defaultGraphConfig, onRefetchGraphClick } = props;

const [graphConfig, setGraphConfig] =
useState<GraphConfig>(defaultGraphConfig);
const [graphConfig, setGraphConfig] = useState<GraphConfig>(defaultGraphConfig);

useEffect(() => {
onGraphConfigChange(graphConfig);
props.onGraphConfigChange(graphConfig);
}, [graphConfig]);

const handleNodeValToggle = (e) => {
if (typeof graphConfig.nodeVal === "function") {
if (typeof graphConfig.nodeVal === 'function') {
setGraphConfig({ ...graphConfig, nodeVal: 1 });
console.log(graphConfig);
} else {
Expand All @@ -34,7 +32,7 @@ const GraphControl = (props) => {
onChange={(e) => {
handleNodeValToggle(e);
}}
checked={typeof graphConfig.nodeVal === "function"}
checked={typeof graphConfig.nodeVal === 'function'}
/>
</Stack>
<Form.Label>Length of links</Form.Label>
Expand Down
Loading

0 comments on commit 9c3ce6b

Please sign in to comment.