Compare commits

...

105 Commits

Author SHA1 Message Date
Alicia Sykes
bc45b91b51 Refactored Dockerfile 2023-08-29 21:47:39 +01:00
Alicia Sykes
6d8b2368e7 Merge branch 'master' of github.com:lissy93/web-check 2023-08-27 21:38:18 +01:00
Alicia Sykes
fbe774829d Updates screenshots 2023-08-27 21:38:07 +01:00
liss-bot
08eadbe02c docs: Updates contributors list 2023-08-27 21:29:01 +01:00
Alicia Sykes
273e2539cc Workflow to insert credits, for contributors and sponsors 2023-08-27 21:28:29 +01:00
Alicia Sykes
6ba75cebf2 Resolve lint warns 2023-08-26 21:31:00 +01:00
Alicia Sykes
98a3243846 Merge pull request #19 from abhishekMuge/master
Issue #16: Eslint unused variable issue removed
2023-08-26 21:28:56 +01:00
Alicia Sykes
f9530f8b03 Merge branch 'master' into master 2023-08-26 21:28:48 +01:00
Alicia Sykes
5145b661f4 Updates screenshot 2023-08-26 21:06:56 +01:00
Alicia Sykes
54dc70e6b2 Adds screenshots as an example 2023-08-26 21:06:17 +01:00
Alicia Sykes
b68447ed60 Adds contents 2023-08-26 17:39:17 +01:00
Alicia Sykes
94281bef08 Updates the screenshot in the readme 2023-08-26 17:28:29 +01:00
Alicia Sykes
56aac52f6b Updates docunentation 2023-08-26 17:27:03 +01:00
Alicia Sykes
e4c99c755e Updates screenshots 2023-08-26 17:26:53 +01:00
Alicia Sykes
7559716c5d Update the start command in the Dockerfile #33 2023-08-26 14:40:20 +01:00
Alicia Sykes
63d09738d2 Update building docs 2023-08-26 14:26:19 +01:00
Alicia Sykes
5496fb6e5c Fix serve command #33 2023-08-26 14:22:44 +01:00
Alicia Sykes
95a63825f0 Merge pull request #41 from Lissy93/FEAT/tls-and-refactor
Feat/tls and refactor
2023-08-26 14:15:03 +01:00
Alicia Sykes
fe6931efc6 Pull upstream, resolve conflicts 2023-08-26 14:14:32 +01:00
Alicia Sykes
db64e5a3d4 Merge pull request #40 from treatmesubj/master-1
phishTank.in_database fix
2023-08-26 14:13:29 +01:00
Alicia Sykes
a779057762 Reorder docs 2023-08-26 13:56:22 +01:00
Alicia Sykes
52cb0fd618 Adds link to ThreatJammer 2023-08-26 13:33:55 +01:00
Alicia Sykes
0194ada819 Update About page and docs 2023-08-26 13:10:09 +01:00
Alicia Sykes
d805848dd7 Re-order fetch requests, and progress bar text to match the UI 2023-08-26 13:09:57 +01:00
Alicia Sykes
749a61358c Improve SSL check, by useing direct TLS connection and listen for secureConnect event 2023-08-26 13:09:08 +01:00
Alicia Sykes
b6b0c25966 Refactor, toggle filters, re-order screenshot 2023-08-25 21:02:28 +01:00
Alicia Sykes
09f5af26df Adds filter functionality to results 2023-08-25 18:08:44 +01:00
Alicia Sykes
805cc41bce TLS features 2023-08-24 20:36:59 +01:00
Alicia Sykes
4bd3085fe9 Adds TLS results, uses str array for IDs, reorders components 2023-08-24 20:36:41 +01:00
Alicia Sykes
823873bce2 Builds result cards for TLS results 2023-08-24 20:36:03 +01:00
Alicia Sykes
49cfad2dbe Update image URL 2023-08-24 20:35:46 +01:00
Alicia Sykes
b6dfd3321a Updates hook to allow for array of job IDs 2023-08-24 20:35:01 +01:00
Alicia Sykes
57481c8757 Updates display of threats view 2023-08-24 12:52:08 +01:00
Alicia Sykes
d13db8d438 Remove mail txt from val 2023-08-24 12:51:01 +01:00
Alicia Sykes
5e755c1dc2 Adds prop for row to also show code or a list 2023-08-24 12:36:01 +01:00
Alicia Sykes
93aa496a30 Adds prop for button to show progress loader 2023-08-24 12:35:46 +01:00
John Hupperts
5dc9a82718 phishTank.in_database fix 2023-08-23 11:20:40 -05:00
Alicia Sykes
86bb64a4d0 Adds an endpoint for fetching TLS info via Moz observetory 2023-08-22 21:40:22 +01:00
Alicia Sykes
f573faf304 Merge pull request #37 from Lissy93/FEAT/malware-checking
Feat/malware checking
2023-08-19 23:52:50 +01:00
Alicia Sykes
737639ae84 Updates the UI for threats from Google Safe Browsing 2023-08-19 23:38:22 +01:00
Alicia Sykes
8ca747c02f Adds support for Google Safe Browsing in threats API endpoint 2023-08-19 23:36:47 +01:00
Alicia Sykes
759bb603df Adds functionality for threats, malware, phishing, viruses 2023-08-19 22:00:15 +01:00
Alicia Sykes
83c8d311b3 UI code for malware checking 2023-08-18 19:34:06 +01:00
Alicia Sykes
93ed8d6c44 Working on lambda function for malware, phishing and viruses check 2023-08-18 19:33:49 +01:00
Alicia Sykes
981f79f676 Fix link to the tech stack image assets 2023-08-18 19:28:44 +01:00
Alicia Sykes
55d59c2d07 Accept an id prop 2023-08-18 19:28:28 +01:00
Alicia Sykes
502b9fd7f4 Flex wrap heading and icon on smaller screens 2023-08-18 19:28:15 +01:00
Alicia Sykes
dbcbd36874 Skip check if robots.txt not present 2023-08-18 19:27:38 +01:00
Alicia Sykes
519d2f0f79 Adds checking for Blocklists 2023-08-18 16:20:16 +01:00
Alicia Sykes
6b8c50a9aa lambda function for checking against DNS blocklists 2023-08-17 23:30:23 +01:00
Alicia Sykes
e24934b7dd Button layout for mobile 2023-08-15 22:25:27 +01:00
Alicia Sykes
645dcf229f Merge branch 'master' of github.com:lissy93/web-check 2023-08-14 22:44:01 +01:00
Alicia Sykes
8ce46fbf89 Updates docs, loader list, react lint and remove commented code 2023-08-14 22:43:51 +01:00
Alicia Sykes
fac47e27c6 Adds react charts 2023-08-14 22:34:55 +01:00
Alicia Sykes
2bf7454950 Improved robustness of whois endpoint 2023-08-14 22:34:38 +01:00
Alicia Sykes
8d4c09ffd9 Implements global page rank widget 2023-08-14 22:15:00 +01:00
Alicia Sykes
c55b23086d Merge pull request #32 from gso-trifork-security/master
Update exposed ports
2023-08-14 10:03:11 +01:00
Alicia Sykes
e2ec9b2f62 Merge pull request #34 from muni106/master
fix url bug
2023-08-14 06:13:37 +01:00
Mounir
07656c6fea fix url bug 2023-08-13 21:39:26 +02:00
Alicia Sykes
fe1e74a22f Adds OpenGraph tags and banner image 2023-08-13 15:19:54 +01:00
Alicia Sykes
cb0143de40 Move cookie logic into cookie component 2023-08-13 13:59:23 +01:00
Alicia Sykes
9430fc7913 Move cookie logic into cookie component 2023-08-13 13:58:51 +01:00
Alicia Sykes
d3fa33b104 Adds unique id attr to mapped elems 2023-08-13 13:58:23 +01:00
Alicia Sykes
a8eadf40b0 Makes SSL check much more comprehensive 2023-08-13 13:57:48 +01:00
Alicia Sykes
9b9c31674d Adds support for client-side cookie identification 2023-08-13 13:57:34 +01:00
Alicia Sykes
d38f2ae384 Fixes changes per day / scan calculation 2023-08-13 13:57:15 +01:00
Alicia Sykes
56ee47fef4 Display results from the Wayback Machine 2023-08-12 21:57:40 +01:00
Alicia Sykes
d63f891667 Adds API endpoint to fetch data from Wayback Machine 2023-08-12 21:57:24 +01:00
Alicia Sykes
8263b9b7fd Integrates the WAF and sec headers checks into UI 2023-08-12 16:08:46 +01:00
Alicia Sykes
b0008823da Adds docs for WAF and HTTP sec headers 2023-08-12 16:08:22 +01:00
Alicia Sykes
1e8d6e868c Adds HTTP security header checks 2023-08-12 16:08:06 +01:00
Alicia Sykes
efba42d59d Adds firewall WAF checks 2023-08-12 16:07:47 +01:00
Alicia Sykes
15e5ba3cfc Updates start script 2023-08-12 16:07:27 +01:00
Gustav Soelberg
e47b39041b Update exposed ports
Ports in use was wrong
2023-08-12 11:27:22 +02:00
Alicia Sykes
6a31927562 Merge pull request #31 from Lissy93/FEAT/node-server
[FEAT] New Server Arch
2023-08-11 23:05:40 +01:00
Alicia Sykes
18faeb631b Adds compatibility for deploying direct to AWS lambda 2023-08-11 22:51:56 +01:00
Alicia Sykes
e1d9b13045 Make API location configurable 2023-08-11 22:51:23 +01:00
Alicia Sykes
c5d8cd1641 Update .gitignore to ignore AWS crap 2023-08-11 22:50:55 +01:00
Alicia Sykes
5f3a99f2b9 Safe fallback when no event query params present 2023-08-11 22:50:37 +01:00
Alicia Sykes
0fd0e537f7 Dockerfile has updated build command, and uses global start 2023-08-10 21:24:52 +01:00
Alicia Sykes
20cc52a304 Server also serves up app to root / path (if enabled) 2023-08-10 21:24:24 +01:00
Alicia Sykes
361c65348d Merge pull request #30 from muni106/fix-warnings
fix current warnings
2023-08-10 19:44:02 +01:00
Alicia Sykes
6cb133a46a Fix header width on 404 page 2023-08-10 19:31:15 +01:00
Alicia Sykes
ca5d43cea1 Increase default execution timeout limit 2023-08-10 19:30:44 +01:00
Alicia Sykes
9e426ed55e Rename endpoints to be the same as job IDs 2023-08-10 19:30:21 +01:00
Mounir
f552e5cb69 fix current warnings 2023-08-10 17:16:11 +02:00
Alicia Sykes
95b13240c7 Makes some functions more robust 2023-08-10 14:27:06 +01:00
Alicia Sykes
f96c7ba25f Implement middleware into newer lambdas 2023-08-10 14:26:36 +01:00
Alicia Sykes
976ca7d47a Serves up compiled react app to base 2023-08-09 23:29:58 +01:00
Alicia Sykes
af1689bd85 Adds support for history router 2023-08-09 23:29:39 +01:00
Alicia Sykes
d2a56eb526 Updates all API endpoints to use common middleware 2023-08-09 22:33:36 +01:00
Alicia Sykes
85af5f9327 Updated middleware to handle various response shapes 2023-08-09 22:31:47 +01:00
Alicia Sykes
8624237760 Merge pull request #24 from muni106/fix-docs-ipAdress
FIX IP ADRESS IN DOCS
2023-08-07 17:56:48 +01:00
Alicia Sykes
f0ff33e081 Starts making middleware functionality 2023-08-06 22:05:14 +01:00
Alicia Sykes
65ff004b63 Fix missing url param when called by server direct 2023-08-06 22:04:57 +01:00
Alicia Sykes
127db45247 Ignore non-files, and use port from env var 2023-08-06 22:04:25 +01:00
Alicia Sykes
30c5dbb898 Adds dotenv for using environmental variables in the node server 2023-08-06 22:03:48 +01:00
Mounir
42eea33809 fix ip adress docs 2023-08-06 16:36:26 +02:00
Alicia Sykes
c46fed5ebb Updates docs 2023-08-05 10:33:05 +01:00
Alicia Sykes
57fadde151 Adds smoothe scroll 2023-08-05 10:32:53 +01:00
abhishek muge
af409245fb warning removed 2023-07-29 13:03:50 +00:00
Alicia Sykes
77d4ca26a4 Merge branch 'master' of github.com:lissy93/web-check into FEAT/node-server 2023-07-28 21:47:39 +01:00
Alicia Sykes
22995995d0 Writes a central server file, to serve up API endpoints 2023-07-28 21:47:07 +01:00
Alicia Sykes
4d69848350 Adds aws-serverlss-express to run without netlify cli 2023-07-28 21:46:47 +01:00
Alicia Sykes
d03acb8a3c Update API endpoints to read URL params from either option 2023-07-28 21:46:20 +01:00
131 changed files with 7133 additions and 1354 deletions

815
.github/README.md vendored

File diff suppressed because it is too large Load Diff

1
.github/screenshots/README.md vendored Normal file
View File

@@ -0,0 +1 @@
![Screenshot](https://raw.githubusercontent.com/Lissy93/web-check/HEAD/.github/screenshots/web-check-screenshot3.png)

BIN
.github/screenshots/tiles/archives.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 95 KiB

BIN
.github/screenshots/tiles/carbon.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

View File

Before

Width:  |  Height:  |  Size: 35 KiB

After

Width:  |  Height:  |  Size: 35 KiB

BIN
.github/screenshots/tiles/dns-server.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

View File

Before

Width:  |  Height:  |  Size: 53 KiB

After

Width:  |  Height:  |  Size: 53 KiB

View File

Before

Width:  |  Height:  |  Size: 165 KiB

After

Width:  |  Height:  |  Size: 165 KiB

View File

Before

Width:  |  Height:  |  Size: 44 KiB

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 67 KiB

View File

Before

Width:  |  Height:  |  Size: 73 KiB

After

Width:  |  Height:  |  Size: 73 KiB

BIN
.github/screenshots/tiles/firewall.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View File

Before

Width:  |  Height:  |  Size: 105 KiB

After

Width:  |  Height:  |  Size: 105 KiB

View File

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 24 KiB

BIN
.github/screenshots/tiles/hsts.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

View File

Before

Width:  |  Height:  |  Size: 94 KiB

After

Width:  |  Height:  |  Size: 94 KiB

View File

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

View File

Before

Width:  |  Height:  |  Size: 158 KiB

After

Width:  |  Height:  |  Size: 158 KiB

BIN
.github/screenshots/tiles/ranking.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

View File

Before

Width:  |  Height:  |  Size: 23 KiB

After

Width:  |  Height:  |  Size: 23 KiB

View File

Before

Width:  |  Height:  |  Size: 114 KiB

After

Width:  |  Height:  |  Size: 114 KiB

BIN
.github/screenshots/tiles/screenshot.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 79 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

View File

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 28 KiB

BIN
.github/screenshots/tiles/sitemap.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 207 KiB

View File

Before

Width:  |  Height:  |  Size: 46 KiB

After

Width:  |  Height:  |  Size: 46 KiB

View File

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 18 KiB

BIN
.github/screenshots/tiles/tech-stack.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 146 KiB

BIN
.github/screenshots/tiles/threats.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 127 KiB

View File

Before

Width:  |  Height:  |  Size: 54 KiB

After

Width:  |  Height:  |  Size: 54 KiB

View File

Before

Width:  |  Height:  |  Size: 123 KiB

After

Width:  |  Height:  |  Size: 123 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 132 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 810 KiB

View File

Before

Width:  |  Height:  |  Size: 2.0 MiB

After

Width:  |  Height:  |  Size: 2.0 MiB

37
.github/workflows/credits.yml vendored Normal file
View File

@@ -0,0 +1,37 @@
# Inserts list of community members into ./README.md
name: 💓 Inserts Contributors & Sponsors
on:
workflow_dispatch: # Manual dispatch
schedule:
- cron: '45 1 * * 0' # At 01:45 on Sunday.
jobs:
# Job #1 - Fetches sponsors and inserts table into readme
insert-sponsors:
runs-on: ubuntu-latest
name: Inserts Sponsors 💓
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Updates readme with sponsors
uses: JamesIves/github-sponsors-readme-action@1.0.5
with:
token: ${{ secrets.BOT_TOKEN || secrets.GITHUB_TOKEN }}
file: .github/README.md
# Job #2 - Fetches contributors and inserts table into readme
insert-contributors:
runs-on: ubuntu-latest
name: Inserts Contributors 💓
steps:
- name: Updates readme with contributors
uses: akhilmhdh/contributors-readme-action@v2.3.4
env:
GITHUB_TOKEN: ${{ secrets.BOT_TOKEN || secrets.GITHUB_TOKEN }}
with:
image_size: 80
readme_path: .github/README.md
columns_per_row: 6
commit_message: 'docs: Updates contributors list'
committer_username: liss-bot
committer_email: liss-bot@d0h.co

25
.gitignore vendored
View File

@@ -1,5 +1,5 @@
# Keys # keys
.env .env
# dependencies # dependencies
@@ -7,22 +7,21 @@
/.pnp /.pnp
.pnp.js .pnp.js
# logs
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# testing # testing
/coverage /coverage
# production # production
/build /build
# misc # Random AWS and Netlify crap
.DS_Store
.env.local
.env.development.local
.env.test.local
.env.production.local
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Local Netlify folder
.netlify .netlify
.serverless
.webpack
# OS generated files
.DS_Store

View File

@@ -1,12 +1,22 @@
FROM node:16-buster-slim AS base FROM node:16-buster-slim
WORKDIR /app
FROM base AS builder
COPY . .
RUN apt-get update && \ RUN apt-get update && \
apt-get install -y chromium traceroute && \ apt-get install -y chromium traceroute && \
chmod 755 /usr/bin/chromium && \ chmod 755 /usr/bin/chromium && \
rm -rf /var/lib/apt/lists/* rm -rf /var/lib/apt/lists/*
RUN npm install --force
EXPOSE 8888 WORKDIR /app
COPY package.json yarn.lock ./
RUN yarn install
COPY . .
RUN yarn build
EXPOSE ${PORT:-3000}
ENV CHROME_PATH='/usr/bin/chromium' ENV CHROME_PATH='/usr/bin/chromium'
CMD ["npm", "run", "serve"]
CMD ["yarn", "serve"]

View File

@@ -0,0 +1,51 @@
const path = require('path');
const nodeExternals = require('webpack-node-externals');
module.exports = {
target: 'node',
mode: 'production',
entry: {
'carbon': './api/carbon.js',
'cookies': './api/cookies.js',
'dns-server': './api/dns-server.js',
'dns': './api/dns.js',
'dnssec': './api/dnssec.js',
'features': './api/features.js',
'get-ip': './api/get-ip.js',
'headers': './api/headers.js',
'hsts': './api/hsts.js',
'linked-pages': './api/linked-pages.js',
'mail-config': './api/mail-config.js',
'ports': './api/ports.js',
'quality': './api/quality.js',
'redirects': './api/redirects.js',
'robots-txt': './api/robots-txt.js',
'screenshot': './api/screenshot.js',
'security-txt': './api/security-txt.js',
'sitemap': './api/sitemap.js',
'social-tags': './api/social-tags.js',
'ssl': './api/ssl.js',
'status': './api/status.js',
'tech-stack': './api/tech-stack.js',
'trace-route': './api/trace-route.js',
'txt-records': './api/txt-records.js',
'whois': './api/whois.js',
},
externals: [nodeExternals()],
output: {
filename: '[name].js',
path: path.resolve(__dirname, '.webpack'),
libraryTarget: 'commonjs2'
},
module: {
rules: [
{
test: /\.js$/,
use: {
loader: 'babel-loader'
},
exclude: /node_modules/,
}
]
}
};

38
api/_common/middleware.js Normal file
View File

@@ -0,0 +1,38 @@
const normalizeUrl = (url) => {
return url.startsWith('http') ? url : `https://${url}`;
};
const commonMiddleware = (handler) => {
return async (event, context, callback) => {
const queryParams = event.queryStringParameters || event.query || {};
const rawUrl = queryParams.url;
if (!rawUrl) {
callback(null, {
statusCode: 500,
body: JSON.stringify({ error: 'No URL specified' }),
});
}
const url = normalizeUrl(rawUrl);
try {
const response = await handler(url, event, context);
if (response.body && response.statusCode) {
callback(null, response);
} else {
callback(null, {
statusCode: 200,
body: typeof response === 'object' ? JSON.stringify(response) : response,
});
}
} catch (error) {
callback(null, {
statusCode: 500,
body: JSON.stringify({ error: error.message }),
});
}
};
};
module.exports = commonMiddleware;

83
api/archives.js Normal file
View File

@@ -0,0 +1,83 @@
const axios = require('axios');
const middleware = require('./_common/middleware');
const convertTimestampToDate = (timestamp) => {
const [year, month, day, hour, minute, second] = [
timestamp.slice(0, 4),
timestamp.slice(4, 6) - 1,
timestamp.slice(6, 8),
timestamp.slice(8, 10),
timestamp.slice(10, 12),
timestamp.slice(12, 14),
].map(num => parseInt(num, 10));
return new Date(year, month, day, hour, minute, second);
}
const countPageChanges = (results) => {
let prevDigest = null;
return results.reduce((acc, curr) => {
if (curr[2] !== prevDigest) {
prevDigest = curr[2];
return acc + 1;
}
return acc;
}, -1);
}
const getAveragePageSize = (scans) => {
const totalSize = scans.map(scan => parseInt(scan[3], 10)).reduce((sum, size) => sum + size, 0);
return Math.round(totalSize / scans.length);
};
const getScanFrequency = (firstScan, lastScan, totalScans, changeCount) => {
const formatToTwoDecimal = num => parseFloat(num.toFixed(2));
const dayFactor = (lastScan - firstScan) / (1000 * 60 * 60 * 24);
const daysBetweenScans = formatToTwoDecimal(dayFactor / totalScans);
const daysBetweenChanges = formatToTwoDecimal(dayFactor / changeCount);
const scansPerDay = formatToTwoDecimal((totalScans - 1) / dayFactor);
const changesPerDay = formatToTwoDecimal(changeCount / dayFactor);
return {
daysBetweenScans,
daysBetweenChanges,
scansPerDay,
changesPerDay,
};
};
const getWaybackData = async (url) => {
const cdxUrl = `https://web.archive.org/cdx/search/cdx?url=${url}&output=json&fl=timestamp,statuscode,digest,length,offset`;
try {
const { data } = await axios.get(cdxUrl);
// Check there's data
if (!data || !Array.isArray(data) || data.length <= 1) {
return { skipped: 'Site has never before been archived via the Wayback Machine' };
}
// Remove the header row
data.shift();
// Process and return the results
const firstScan = convertTimestampToDate(data[0][0]);
const lastScan = convertTimestampToDate(data[data.length - 1][0]);
const totalScans = data.length;
const changeCount = countPageChanges(data);
return {
firstScan,
lastScan,
totalScans,
changeCount,
averagePageSize: getAveragePageSize(data),
scanFrequency: getScanFrequency(firstScan, lastScan, totalScans, changeCount),
scans: data,
scanUrl: url,
};
} catch (err) {
return { error: `Error fetching Wayback data: ${err.message}` };
}
};
exports.handler = middleware(getWaybackData);

105
api/block-lists.js Normal file
View File

@@ -0,0 +1,105 @@
const dns = require('dns');
const { URL } = require('url');
const middleware = require('./_common/middleware');
const DNS_SERVERS = [
{ name: 'AdGuard', ip: '176.103.130.130' },
{ name: 'AdGuard Family', ip: '176.103.130.132' },
{ name: 'CleanBrowsing Adult', ip: '185.228.168.10' },
{ name: 'CleanBrowsing Family', ip: '185.228.168.168' },
{ name: 'CleanBrowsing Security', ip: '185.228.168.9' },
{ name: 'CloudFlare', ip: '1.1.1.1' },
{ name: 'CloudFlare Family', ip: '1.1.1.3' },
{ name: 'Comodo Secure', ip: '8.26.56.26' },
{ name: 'Google DNS', ip: '8.8.8.8' },
{ name: 'Neustar Family', ip: '156.154.70.3' },
{ name: 'Neustar Protection', ip: '156.154.70.2' },
{ name: 'Norton Family', ip: '199.85.126.20' },
{ name: 'OpenDNS', ip: '208.67.222.222' },
{ name: 'OpenDNS Family', ip: '208.67.222.123' },
{ name: 'Quad9', ip: '9.9.9.9' },
{ name: 'Yandex Family', ip: '77.88.8.7' },
{ name: 'Yandex Safe', ip: '77.88.8.88' },
];
const knownBlockIPs = [
'146.112.61.106', // OpenDNS
'185.228.168.10', // CleanBrowsing
'8.26.56.26', // Comodo
'9.9.9.9', // Quad9
'208.69.38.170', // Some OpenDNS IPs
'208.69.39.170', // Some OpenDNS IPs
'208.67.222.222', // OpenDNS
'208.67.222.123', // OpenDNS FamilyShield
'199.85.126.10', // Norton
'199.85.126.20', // Norton Family
'156.154.70.22', // Neustar
'77.88.8.7', // Yandex
'77.88.8.8', // Yandex
'::1', // Localhost IPv6
'2a02:6b8::feed:0ff', // Yandex DNS
'2a02:6b8::feed:bad', // Yandex Safe
'2a02:6b8::feed:a11', // Yandex Family
'2620:119:35::35', // OpenDNS
'2620:119:53::53', // OpenDNS FamilyShield
'2606:4700:4700::1111', // Cloudflare
'2606:4700:4700::1001', // Cloudflare
'2001:4860:4860::8888', // Google DNS
'2a0d:2a00:1::', // AdGuard
'2a0d:2a00:2::' // AdGuard Family
];
const isDomainBlocked = async (domain, serverIP) => {
return new Promise((resolve) => {
dns.resolve4(domain, { server: serverIP }, (err, addresses) => {
if (!err) {
if (addresses.some(addr => knownBlockIPs.includes(addr))) {
resolve(true);
return;
}
resolve(false);
return;
}
dns.resolve6(domain, { server: serverIP }, (err6, addresses6) => {
if (!err6) {
if (addresses6.some(addr => knownBlockIPs.includes(addr))) {
resolve(true);
return;
}
resolve(false);
return;
}
if (err6.code === 'ENOTFOUND' || err6.code === 'SERVFAIL') {
resolve(true);
} else {
resolve(false);
}
});
});
});
};
const checkDomainAgainstDnsServers = async (domain) => {
let results = [];
for (let server of DNS_SERVERS) {
const isBlocked = await isDomainBlocked(domain, server.ip);
results.push({
server: server.name,
serverIp: server.ip,
isBlocked,
});
}
return results;
};
exports.handler = middleware(async (url) => {
const domain = new URL(url).hostname;
const results = await checkDomainAgainstDnsServers(domain);
return {
statusCode: 200,
body: JSON.stringify({ blocklists: results })
};
});

View File

@@ -1,14 +1,7 @@
const https = require('https'); const https = require('https');
const middleware = require('./_common/middleware');
exports.handler = async (event, context) => { const handler = async (url) => {
const { url } = event.queryStringParameters;
if (!url) {
return {
statusCode: 400,
body: JSON.stringify({ error: 'url query parameter is required' }),
};
}
// First, get the size of the website's HTML // First, get the size of the website's HTML
const getHtmlSize = (url) => new Promise((resolve, reject) => { const getHtmlSize = (url) => new Promise((resolve, reject) => {
@@ -49,14 +42,10 @@ exports.handler = async (event, context) => {
} }
carbonData.scanUrl = url; carbonData.scanUrl = url;
return { return carbonData;
statusCode: 200,
body: JSON.stringify(carbonData),
};
} catch (error) { } catch (error) {
return { throw new Error(`Error: ${error.message}`);
statusCode: 500,
body: JSON.stringify({ error: `Error: ${error.message}` }),
};
} }
}; };
exports.handler = middleware(handler);

View File

@@ -1,60 +0,0 @@
const axios = require('axios');
const cheerio = require('cheerio');
const urlLib = require('url');
exports.handler = async (event, context) => {
let url = event.queryStringParameters.url;
// Check if url includes protocol
if (!url.startsWith('http://') && !url.startsWith('https://')) {
url = 'http://' + url;
}
try {
const response = await axios.get(url);
const html = response.data;
const $ = cheerio.load(html);
const internalLinksMap = new Map();
const externalLinksMap = new Map();
$('a[href]').each((i, link) => {
const href = $(link).attr('href');
const absoluteUrl = urlLib.resolve(url, href);
if (absoluteUrl.startsWith(url)) {
const count = internalLinksMap.get(absoluteUrl) || 0;
internalLinksMap.set(absoluteUrl, count + 1);
} else if (href.startsWith('http://') || href.startsWith('https://')) {
const count = externalLinksMap.get(absoluteUrl) || 0;
externalLinksMap.set(absoluteUrl, count + 1);
}
});
// Convert maps to sorted arrays
const internalLinks = [...internalLinksMap.entries()].sort((a, b) => b[1] - a[1]).map(entry => entry[0]);
const externalLinks = [...externalLinksMap.entries()].sort((a, b) => b[1] - a[1]).map(entry => entry[0]);
if (internalLinks.length === 0 && externalLinks.length === 0) {
return {
statusCode: 400,
body: JSON.stringify({
skipped: 'No internal or external links found. '
+ 'This may be due to the website being dynamically rendered, using a client-side framework (like React), and without SSR enabled. '
+ 'That would mean that the static HTML returned from the HTTP request doesn\'t contain any meaningful content for Web-Check to analyze. '
+ 'You can rectify this by using a headless browser to render the page instead.',
}),
};
}
return {
statusCode: 200,
body: JSON.stringify({ internal: internalLinks, external: externalLinks }),
};
} catch (error) {
console.log(error);
return {
statusCode: 500,
body: JSON.stringify({ error: 'Failed fetching data' }),
};
}
};

57
api/cookies.js Normal file
View File

@@ -0,0 +1,57 @@
const axios = require('axios');
const puppeteer = require('puppeteer');
const middleware = require('./_common/middleware');
const getPuppeteerCookies = async (url) => {
const browser = await puppeteer.launch({
headless: 'new',
args: ['--no-sandbox', '--disable-setuid-sandbox'],
});
try {
const page = await browser.newPage();
const navigationPromise = page.goto(url, { waitUntil: 'networkidle2' });
const timeoutPromise = new Promise((_, reject) =>
setTimeout(() => reject(new Error('Puppeteer took too long!')), 3000)
);
await Promise.race([navigationPromise, timeoutPromise]);
return await page.cookies();
} finally {
await browser.close();
}
};
const handler = async (url) => {
let headerCookies = null;
let clientCookies = null;
try {
const response = await axios.get(url, {
withCredentials: true,
maxRedirects: 5,
});
headerCookies = response.headers['set-cookie'];
} catch (error) {
if (error.response) {
return { error: `Request failed with status ${error.response.status}: ${error.message}` };
} else if (error.request) {
return { error: `No response received: ${error.message}` };
} else {
return { error: `Error setting up request: ${error.message}` };
}
}
try {
clientCookies = await getPuppeteerCookies(url);
} catch (_) {
clientCookies = null;
}
if (!headerCookies && (!clientCookies || clientCookies.length === 0)) {
return { skipped: 'No cookies' };
}
return { headerCookies, clientCookies };
};
exports.handler = middleware(handler);

View File

@@ -1,11 +1,11 @@
const dns = require('dns'); const dns = require('dns');
const dnsPromises = dns.promises; const dnsPromises = dns.promises;
// const https = require('https');
const axios = require('axios'); const axios = require('axios');
const commonMiddleware = require('./_common/middleware');
exports.handler = async (event) => { const handler = async (url) => {
const domain = event.queryStringParameters.url.replace(/^(?:https?:\/\/)?/i, "");
try { try {
const domain = url.replace(/^(?:https?:\/\/)?/i, "");
const addresses = await dnsPromises.resolve4(domain); const addresses = await dnsPromises.resolve4(domain);
const results = await Promise.all(addresses.map(async (address) => { const results = await Promise.all(addresses.map(async (address) => {
const hostname = await dnsPromises.reverse(address).catch(() => null); const hostname = await dnsPromises.reverse(address).catch(() => null);
@@ -22,6 +22,7 @@ exports.handler = async (event) => {
dohDirectSupports, dohDirectSupports,
}; };
})); }));
// let dohMozillaSupport = false; // let dohMozillaSupport = false;
// try { // try {
// const mozillaList = await axios.get('https://firefox.settings.services.mozilla.com/v1/buckets/security-state/collections/onecrl/records'); // const mozillaList = await axios.get('https://firefox.settings.services.mozilla.com/v1/buckets/security-state/collections/onecrl/records');
@@ -29,20 +30,15 @@ exports.handler = async (event) => {
// } catch (error) { // } catch (error) {
// console.error(error); // console.error(error);
// } // }
return { return {
statusCode: 200, domain,
body: JSON.stringify({ dns: results,
domain, // dohMozillaSupport,
dns: results,
// dohMozillaSupport,
}),
}; };
} catch (error) { } catch (error) {
return { throw new Error(`An error occurred while resolving DNS. ${error.message}`); // This will be caught and handled by the commonMiddleware
statusCode: 500,
body: JSON.stringify({
error: `An error occurred while resolving DNS. ${error.message}`,
}),
};
} }
}; };
exports.handler = commonMiddleware(handler);

View File

@@ -1,8 +1,9 @@
const dns = require('dns'); const dns = require('dns');
const util = require('util'); const util = require('util');
const middleware = require('./_common/middleware');
exports.handler = async function(event, context) { const handler = async (url) => {
let hostname = event.queryStringParameters.url; let hostname = url;
// Handle URLs by extracting hostname // Handle URLs by extracting hostname
if (hostname.startsWith('http://') || hostname.startsWith('https://')) { if (hostname.startsWith('http://') || hostname.startsWith('https://')) {
@@ -35,25 +36,19 @@ exports.handler = async function(event, context) {
]); ]);
return { return {
statusCode: 200, A: a,
body: JSON.stringify({ AAAA: aaaa,
A: a, MX: mx,
AAAA: aaaa, TXT: txt,
MX: mx, NS: ns,
TXT: txt, CNAME: cname,
NS: ns, SOA: soa,
CNAME: cname, SRV: srv,
SOA: soa, PTR: ptr
SRV: srv,
PTR: ptr
})
}; };
} catch (error) { } catch (error) {
return { throw new Error(error.message);
statusCode: 500,
body: JSON.stringify({
error: error.message
})
};
} }
}; };
exports.handler = middleware(handler);

View File

@@ -1,16 +1,7 @@
const https = require('https'); const https = require('https');
const commonMiddleware = require('./_common/middleware'); // Make sure this path is correct
exports.handler = async function(event, context) { const fetchDNSRecords = async (domain, event, context) => {
let { url } = event.queryStringParameters;
if (!url) {
return errorResponse('URL query parameter is required.');
}
// Extract hostname from URL
const parsedUrl = new URL(url);
const domain = parsedUrl.hostname;
const dnsTypes = ['DNSKEY', 'DS', 'RRSIG']; const dnsTypes = ['DNSKEY', 'DS', 'RRSIG'];
const records = {}; const records = {};
@@ -48,22 +39,14 @@ exports.handler = async function(event, context) {
if (dnsResponse.Answer) { if (dnsResponse.Answer) {
records[type] = { isFound: true, answer: dnsResponse.Answer, response: dnsResponse.Answer }; records[type] = { isFound: true, answer: dnsResponse.Answer, response: dnsResponse.Answer };
} else { } else {
records[type] = { isFound: false, answer: null, response: dnsResponse}; records[type] = { isFound: false, answer: null, response: dnsResponse };
} }
} catch (error) { } catch (error) {
return errorResponse(`Error fetching ${type} record: ${error.message}`); throw new Error(`Error fetching ${type} record: ${error.message}`); // This will be caught and handled by the commonMiddleware
} }
} }
return { return records;
statusCode: 200,
body: JSON.stringify(records),
};
}; };
const errorResponse = (message, statusCode = 444) => { exports.handler = commonMiddleware(fetchDNSRecords);
return {
statusCode: statusCode,
body: JSON.stringify({ error: message }),
};
};

View File

@@ -1,22 +1,15 @@
const https = require('https'); const https = require('https');
const middleware = require('./_common/middleware');
exports.handler = async function (event, context) { const builtWithHandler = async (url) => {
const { url } = event.queryStringParameters;
const apiKey = process.env.BUILT_WITH_API_KEY; const apiKey = process.env.BUILT_WITH_API_KEY;
const errorResponse = (message, statusCode = 500) => {
return {
statusCode: statusCode,
body: JSON.stringify({ error: message }),
};
};
if (!url) { if (!url) {
return errorResponse('URL query parameter is required', 400); throw new Error('URL query parameter is required');
} }
if (!apiKey) { if (!apiKey) {
return errorResponse('Missing BuiltWith API key in environment variables', 500); throw new Error('Missing BuiltWith API key in environment variables');
} }
const apiUrl = `https://api.builtwith.com/free1/api.json?KEY=${apiKey}&LOOKUP=${encodeURIComponent(url)}`; const apiUrl = `https://api.builtwith.com/free1/api.json?KEY=${apiKey}&LOOKUP=${encodeURIComponent(url)}`;
@@ -46,11 +39,10 @@ exports.handler = async function (event, context) {
req.end(); req.end();
}); });
return { return response;
statusCode: 200,
body: response,
};
} catch (error) { } catch (error) {
return errorResponse(`Error making request: ${error.message}`); throw new Error(`Error making request: ${error.message}`);
} }
}; };
exports.handler = middleware(builtWithHandler);

View File

@@ -1,33 +0,0 @@
const dns = require('dns');
/* Lambda function to fetch the IP address of a given URL */
exports.handler = function (event, context, callback) {
const addressParam = event.queryStringParameters.url;
if (!addressParam) {
callback(null, errorResponse('Address parameter is missing.'));
return;
}
const address = decodeURIComponent(addressParam)
.replaceAll('https://', '')
.replaceAll('http://', '');
dns.lookup(address, (err, ip, family) => {
if (err) {
callback(null, errorResponse(err.message));
} else {
callback(null, {
statusCode: 200,
body: JSON.stringify({ ip, family }),
});
}
});
};
const errorResponse = (message, statusCode = 444) => {
return {
statusCode: statusCode,
body: JSON.stringify({ error: message }),
};
};

105
api/firewall.js Normal file
View File

@@ -0,0 +1,105 @@
const axios = require('axios');
const middleware = require('./_common/middleware');
const hasWaf = (waf) => {
return {
hasWaf: true, waf,
}
};
const handler = async (url) => {
const fullUrl = url.startsWith('http') ? url : `http://${url}`;
try {
const response = await axios.get(fullUrl);
const headers = response.headers;
if (headers['server'] && headers['server'].includes('cloudflare')) {
return hasWaf('Cloudflare');
}
if (headers['x-powered-by'] && headers['x-powered-by'].includes('AWS Lambda')) {
return hasWaf('AWS WAF');
}
if (headers['server'] && headers['server'].includes('AkamaiGHost')) {
return hasWaf('Akamai');
}
if (headers['server'] && headers['server'].includes('Sucuri')) {
return hasWaf('Sucuri');
}
if (headers['server'] && headers['server'].includes('BarracudaWAF')) {
return hasWaf('Barracuda WAF');
}
if (headers['server'] && (headers['server'].includes('F5 BIG-IP') || headers['server'].includes('BIG-IP'))) {
return hasWaf('F5 BIG-IP');
}
if (headers['x-sucuri-id'] || headers['x-sucuri-cache']) {
return hasWaf('Sucuri CloudProxy WAF');
}
if (headers['server'] && headers['server'].includes('FortiWeb')) {
return hasWaf('Fortinet FortiWeb WAF');
}
if (headers['server'] && headers['server'].includes('Imperva')) {
return hasWaf('Imperva SecureSphere WAF');
}
if (headers['x-protected-by'] && headers['x-protected-by'].includes('Sqreen')) {
return hasWaf('Sqreen');
}
if (headers['x-waf-event-info']) {
return hasWaf('Reblaze WAF');
}
if (headers['set-cookie'] && headers['set-cookie'].includes('_citrix_ns_id')) {
return hasWaf('Citrix NetScaler');
}
if (headers['x-denied-reason'] || headers['x-wzws-requested-method']) {
return hasWaf('WangZhanBao WAF');
}
if (headers['x-webcoment']) {
return hasWaf('Webcoment Firewall');
}
if (headers['server'] && headers['server'].includes('Yundun')) {
return hasWaf('Yundun WAF');
}
if (headers['x-yd-waf-info'] || headers['x-yd-info']) {
return hasWaf('Yundun WAF');
}
if (headers['server'] && headers['server'].includes('Safe3WAF')) {
return hasWaf('Safe3 Web Application Firewall');
}
if (headers['server'] && headers['server'].includes('NAXSI')) {
return hasWaf('NAXSI WAF');
}
if (headers['x-datapower-transactionid']) {
return hasWaf('IBM WebSphere DataPower');
}
return {
hasWaf: false,
}
} catch (error) {
return {
statusCode: 500,
body: JSON.stringify({ error: error.message }),
};
}
};
exports.handler = middleware(handler);

View File

@@ -1,35 +0,0 @@
exports.handler = async (event) => {
const { url } = event.queryStringParameters;
const redirects = [url];
try {
const got = await import('got');
await got.default(url, {
followRedirect: true,
maxRedirects: 12,
hooks: {
beforeRedirect: [
(options, response) => {
redirects.push(response.headers.location);
},
],
},
});
return {
statusCode: 200,
body: JSON.stringify({
redirects: redirects,
}),
};
} catch (error) {
return errorResponse(`Error: ${error.message}`);
}
};
const errorResponse = (message, statusCode = 444) => {
return {
statusCode: statusCode,
body: JSON.stringify({ error: message }),
};
};

View File

@@ -1,26 +0,0 @@
const axios = require('axios');
exports.handler = async function(event, context) {
const { url } = event.queryStringParameters;
if (!url) {
return {
statusCode: 400,
body: JSON.stringify({ message: 'url query string parameter is required' }),
};
}
try {
const response = await axios.get(url, {withCredentials: true});
const cookies = response.headers['set-cookie'];
return {
statusCode: 200,
body: JSON.stringify({ cookies }),
};
} catch (error) {
return {
statusCode: 500,
body: JSON.stringify({ error: error.message }),
};
}
};

View File

@@ -1,31 +0,0 @@
const axios = require('axios');
exports.handler = async function(event, context) {
const { url } = event.queryStringParameters;
if (!url) {
return {
statusCode: 400,
body: JSON.stringify({ error: 'url query string parameter is required' }),
};
}
try {
const response = await axios.get(url, {
validateStatus: function (status) {
return status >= 200 && status < 600; // Resolve only if the status code is less than 600
},
});
return {
statusCode: 200,
body: JSON.stringify(response.headers),
};
} catch (error) {
console.log(error);
return {
statusCode: 500,
body: JSON.stringify({ error: error.message }),
};
}
};

21
api/get-ip.js Normal file
View File

@@ -0,0 +1,21 @@
const dns = require('dns');
const middleware = require('./_common/middleware');
const lookupAsync = (address) => {
return new Promise((resolve, reject) => {
dns.lookup(address, (err, ip, family) => {
if (err) {
reject(err);
} else {
resolve({ ip, family });
}
});
});
};
const handler = async (url) => {
const address = url.replaceAll('https://', '').replaceAll('http://', '');
return await lookupAsync(address);
};
exports.handler = middleware(handler);

18
api/headers.js Normal file
View File

@@ -0,0 +1,18 @@
const axios = require('axios');
const middleware = require('./_common/middleware');
const handler = async (url, event, context) => {
try {
const response = await axios.get(url, {
validateStatus: function (status) {
return status >= 200 && status < 600; // Resolve only if the status code is less than 600
},
});
return response.headers;
} catch (error) {
throw new Error(error.message);
}
};
exports.handler = middleware(handler);

View File

@@ -1,8 +1,7 @@
const https = require('https'); const https = require('https');
const middleware = require('./_common/middleware');
exports.handler = async function(event, context) { exports.handler = middleware(async (url, event, context) => {
const siteURL = event.queryStringParameters.url;
const errorResponse = (message, statusCode = 500) => { const errorResponse = (message, statusCode = 500) => {
return { return {
statusCode: statusCode, statusCode: statusCode,
@@ -16,15 +15,9 @@ exports.handler = async function(event, context) {
}; };
}; };
if (!siteURL) {
return {
statusCode: 400,
body: JSON.stringify({ error: 'URL parameter is missing!' }),
};
}
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const req = https.request(siteURL, res => { const req = https.request(url, res => {
const headers = res.headers; const headers = res.headers;
const hstsHeader = headers['strict-transport-security']; const hstsHeader = headers['strict-transport-security'];
@@ -60,4 +53,5 @@ exports.handler = async function(event, context) {
req.end(); req.end();
}); });
}; });

25
api/http-security.js Normal file
View File

@@ -0,0 +1,25 @@
const axios = require('axios');
const middleware = require('./_common/middleware');
const handler = async (url) => {
const fullUrl = url.startsWith('http') ? url : `http://${url}`;
try {
const response = await axios.get(fullUrl);
const headers = response.headers;
return {
strictTransportPolicy: headers['strict-transport-policy'] ? true : false,
xFrameOptions: headers['x-frame-options'] ? true : false,
xContentTypeOptions: headers['x-content-type-options'] ? true : false,
xXSSProtection: headers['x-xss-protection'] ? true : false,
contentSecurityPolicy: headers['content-security-policy'] ? true : false,
}
} catch (error) {
return {
statusCode: 500,
body: JSON.stringify({ error: error.message }),
};
}
};
exports.handler = middleware(handler);

70
api/legacy-rank.js Normal file
View File

@@ -0,0 +1,70 @@
const axios = require('axios');
const unzipper = require('unzipper');
const csv = require('csv-parser');
const fs = require('fs');
const middleware = require('./_common/middleware');
// Should also work with the following sources:
// https://www.domcop.com/files/top/top10milliondomains.csv.zip
// https://tranco-list.eu/top-1m.csv.zip
// https://www.domcop.com/files/top/top10milliondomains.csv.zip
// https://radar.cloudflare.com/charts/LargerTopDomainsTable/attachment?id=525&top=1000000
// https://statvoo.com/dl/top-1million-sites.csv.zip
const FILE_URL = 'https://s3-us-west-1.amazonaws.com/umbrella-static/top-1m.csv.zip';
const TEMP_FILE_PATH = '/tmp/top-1m.csv';
const handler = async (url) => {
let domain = null;
try {
domain = new URL(url).hostname;
} catch (e) {
throw new Error('Invalid URL');
}
// Download and unzip the file if not in cache
if (!fs.existsSync(TEMP_FILE_PATH)) {
const response = await axios({
method: 'GET',
url: FILE_URL,
responseType: 'stream'
});
await new Promise((resolve, reject) => {
response.data
.pipe(unzipper.Extract({ path: '/tmp' }))
.on('close', resolve)
.on('error', reject);
});
}
// Parse the CSV and find the rank
return new Promise((resolve, reject) => {
const csvStream = fs.createReadStream(TEMP_FILE_PATH)
.pipe(csv({
headers: ['rank', 'domain'],
}))
.on('data', (row) => {
if (row.domain === domain) {
csvStream.destroy();
resolve({
domain: domain,
rank: row.rank,
isFound: true,
});
}
})
.on('end', () => {
resolve({
skipped: `Skipping, as ${domain} is not present in the Umbrella top 1M list.`,
domain: domain,
isFound: false,
});
})
.on('error', reject);
});
};
exports.handler = middleware(handler);

View File

@@ -1,40 +0,0 @@
const axios = require('axios');
exports.handler = function(event, context, callback) {
const { url } = event.queryStringParameters;
if (!url) {
callback(null, {
statusCode: 400,
body: JSON.stringify({ error: 'URL param is required'}),
});
}
const apiKey = process.env.GOOGLE_CLOUD_API_KEY;
if (!apiKey) {
callback(null, {
statusCode: 500,
body: JSON.stringify({ error: 'API key (GOOGLE_CLOUD_API_KEY) not set'}),
});
}
const endpoint = `https://www.googleapis.com/pagespeedonline/v5/runPagespeed?url=${encodeURIComponent(url)}&category=PERFORMANCE&category=ACCESSIBILITY&category=BEST_PRACTICES&category=SEO&category=PWA&strategy=mobile&key=${apiKey}`;
axios.get(endpoint)
.then(
(response) => {
callback(null, {
statusCode: 200,
body: JSON.stringify(response.data),
});
}
).catch(
() => {
callback(null, {
statusCode: 500,
body: JSON.stringify({ error: 'Error running Lighthouse'}),
});
}
);
};

48
api/linked-pages.js Normal file
View File

@@ -0,0 +1,48 @@
const axios = require('axios');
const cheerio = require('cheerio');
const urlLib = require('url');
const commonMiddleware = require('./_common/middleware');
const handler = async (url) => {
const response = await axios.get(url);
const html = response.data;
const $ = cheerio.load(html);
const internalLinksMap = new Map();
const externalLinksMap = new Map();
// Get all links on the page
$('a[href]').each((i, link) => {
const href = $(link).attr('href');
const absoluteUrl = urlLib.resolve(url, href);
// Check if absolute / relative, append to appropriate map or increment occurrence count
if (absoluteUrl.startsWith(url)) {
const count = internalLinksMap.get(absoluteUrl) || 0;
internalLinksMap.set(absoluteUrl, count + 1);
} else if (href.startsWith('http://') || href.startsWith('https://')) {
const count = externalLinksMap.get(absoluteUrl) || 0;
externalLinksMap.set(absoluteUrl, count + 1);
}
});
// Sort by most occurrences, remove supplicates, and convert to array
const internalLinks = [...internalLinksMap.entries()].sort((a, b) => b[1] - a[1]).map(entry => entry[0]);
const externalLinks = [...externalLinksMap.entries()].sort((a, b) => b[1] - a[1]).map(entry => entry[0]);
// If there were no links, then mark as skipped and show reasons
if (internalLinks.length === 0 && externalLinks.length === 0) {
return {
statusCode: 400,
body: JSON.stringify({
skipped: 'No internal or external links found. '
+ 'This may be due to the website being dynamically rendered, using a client-side framework (like React), and without SSR enabled. '
+ 'That would mean that the static HTML returned from the HTTP request doesn\'t contain any meaningful content for Web-Check to analyze. '
+ 'You can rectify this by using a headless browser to render the page instead.',
}),
};
}
return { internal: internalLinks, external: externalLinks };
};
exports.handler = commonMiddleware(handler);

View File

@@ -1,11 +1,11 @@
const commonMiddleware = require('./_common/middleware');
const dns = require('dns').promises; const dns = require('dns').promises;
const URL = require('url-parse'); const URL = require('url-parse');
exports.handler = async (event, context) => { const handler = async (url, event, context) => {
try { try {
let domain = event.queryStringParameters.url; const domain = new URL(url).hostname || new URL(url).pathname;
const parsedUrl = new URL(domain);
domain = parsedUrl.hostname || parsedUrl.pathname;
// Get MX records // Get MX records
const mxRecords = await dns.resolveMx(domain); const mxRecords = await dns.resolveMx(domain);
@@ -56,19 +56,13 @@ exports.handler = async (event, context) => {
} }
return { return {
statusCode: 200,
body: JSON.stringify({
mxRecords, mxRecords,
txtRecords: emailTxtRecords, txtRecords: emailTxtRecords,
mailServices, mailServices,
}), };
};
} catch (error) { } catch (error) {
if (error.code === 'ENOTFOUND' || error.code === 'ENODATA') { if (error.code === 'ENOTFOUND' || error.code === 'ENODATA') {
return { return { skipped: 'No mail server in use on this domain' };
statusCode: 200,
body: JSON.stringify({ skipped: 'No mail server in use on this domain' }),
};
} else { } else {
return { return {
statusCode: 500, statusCode: 500,
@@ -77,3 +71,5 @@ exports.handler = async (event, context) => {
} }
} }
}; };
module.exports.handler = commonMiddleware(handler);

View File

@@ -1,4 +1,5 @@
const net = require('net'); const net = require('net');
const middleware = require('./_common/middleware');
// A list of commonly used ports. // A list of commonly used ports.
const PORTS = [ const PORTS = [
@@ -12,7 +13,7 @@ async function checkPort(port, domain) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const socket = new net.Socket(); const socket = new net.Socket();
socket.setTimeout(1500); // you may want to adjust the timeout socket.setTimeout(1500);
socket.once('connect', () => { socket.once('connect', () => {
socket.destroy(); socket.destroy();
@@ -33,13 +34,9 @@ async function checkPort(port, domain) {
}); });
} }
exports.handler = async (event, context) => { const handler = async (url, event, context) => {
const domain = event.queryStringParameters.url; const domain = url.replace(/(^\w+:|^)\/\//, '');
if (!domain) {
return errorResponse('Missing domain parameter.');
}
const delay = ms => new Promise(res => setTimeout(res, ms)); const delay = ms => new Promise(res => setTimeout(res, ms));
const timeout = delay(9000); const timeout = delay(9000);
@@ -88,3 +85,5 @@ const errorResponse = (message, statusCode = 444) => {
body: JSON.stringify({ error: message }), body: JSON.stringify({ error: message }),
}; };
}; };
exports.handler = middleware(handler);

22
api/quality.js Normal file
View File

@@ -0,0 +1,22 @@
const axios = require('axios');
const middleware = require('./_common/middleware');
const handler = async (url, event, context) => {
const apiKey = process.env.GOOGLE_CLOUD_API_KEY;
if (!url) {
throw new Error('URL param is required');
}
if (!apiKey) {
throw new Error('API key (GOOGLE_CLOUD_API_KEY) not set');
}
const endpoint = `https://www.googleapis.com/pagespeedonline/v5/runPagespeed?url=${encodeURIComponent(url)}&category=PERFORMANCE&category=ACCESSIBILITY&category=BEST_PRACTICES&category=SEO&category=PWA&strategy=mobile&key=${apiKey}`;
const response = await axios.get(endpoint);
return response.data;
};
exports.handler = middleware(handler);

24
api/rank.js Normal file
View File

@@ -0,0 +1,24 @@
const axios = require('axios');
const middleware = require('./_common/middleware');
const handler = async (url) => {
const domain = url ? new URL(url).hostname : null;
if (!domain) throw new Error('Invalid URL');
try {
const auth = process.env.TRANCO_API_KEY ? // Auth is optional.
{ auth: { username: process.env.TRANCO_USERNAME, password: process.env.TRANCO_API_KEY } }
: {};
const response = await axios.get(
`https://tranco-list.eu/api/ranks/domain/${domain}`, { timeout: 2000 }, auth,
);
if (!response.data || !response.data.ranks || response.data.ranks.length === 0) {
return { skipped: `Skipping, as ${domain} isn't ranked in the top 100 million sites yet.`};
}
return response.data;
} catch (error) {
return { error: `Unable to fetch rank, ${error.message}` };
}
};
exports.handler = middleware(handler);

View File

@@ -1,45 +0,0 @@
const axios = require('axios');
exports.handler = async function(event, context) {
const siteURL = event.queryStringParameters.url;
if (!siteURL) {
return {
statusCode: 400,
body: JSON.stringify({ error: 'Missing url query parameter' }),
};
}
let parsedURL;
try {
parsedURL = new URL(siteURL);
} catch (error) {
return {
statusCode: 400,
body: JSON.stringify({ error: 'Invalid url query parameter' }),
};
}
const robotsURL = `${parsedURL.protocol}//${parsedURL.hostname}/robots.txt`;
try {
const response = await axios.get(robotsURL);
if (response.status === 200) {
return {
statusCode: 200,
body: response.data,
};
} else {
return {
statusCode: response.status,
body: JSON.stringify({ error: 'Failed to fetch robots.txt', statusCode: response.status }),
};
}
} catch (error) {
return {
statusCode: 500,
body: JSON.stringify({ error: `Error fetching robots.txt: ${error.message}` }),
};
}
};

27
api/redirects.js Normal file
View File

@@ -0,0 +1,27 @@
const handler = async (url) => {
const redirects = [url];
const got = await import('got');
try {
await got.default(url, {
followRedirect: true,
maxRedirects: 12,
hooks: {
beforeRedirect: [
(options, response) => {
redirects.push(response.headers.location);
},
],
},
});
return {
redirects: redirects,
};
} catch (error) {
throw new Error(`Error: ${error.message}`);
}
};
const middleware = require('./_common/middleware');
exports.handler = middleware(handler);

70
api/robots-txt.js Normal file
View File

@@ -0,0 +1,70 @@
const axios = require('axios');
const middleware = require('./_common/middleware');
const parseRobotsTxt = (content) => {
const lines = content.split('\n');
const rules = [];
lines.forEach(line => {
line = line.trim(); // This removes trailing and leading whitespaces
let match = line.match(/^(Allow|Disallow):\s*(\S*)$/i);
if (match) {
const rule = {
lbl: match[1],
val: match[2],
};
rules.push(rule);
} else {
match = line.match(/^(User-agent):\s*(\S*)$/i);
if (match) {
const rule = {
lbl: match[1],
val: match[2],
};
rules.push(rule);
}
}
});
return { robots: rules };
}
const handler = async function(url) {
let parsedURL;
try {
parsedURL = new URL(url);
} catch (error) {
return {
statusCode: 400,
body: JSON.stringify({ error: 'Invalid url query parameter' }),
};
}
const robotsURL = `${parsedURL.protocol}//${parsedURL.hostname}/robots.txt`;
try {
const response = await axios.get(robotsURL);
if (response.status === 200) {
const parsedData = parseRobotsTxt(response.data);
if (!parsedData.robots || parsedData.robots.length === 0) {
return { skipped: 'No robots.txt file present, unable to continue' };
}
return parsedData;
} else {
return {
statusCode: response.status,
body: JSON.stringify({ error: 'Failed to fetch robots.txt', statusCode: response.status }),
};
}
} catch (error) {
return {
statusCode: 500,
body: JSON.stringify({ error: `Error fetching robots.txt: ${error.message}` }),
};
}
};
exports.handler = middleware(handler);

View File

@@ -1,16 +1,11 @@
const puppeteer = require('puppeteer-core'); const puppeteer = require('puppeteer-core');
const chromium = require('chrome-aws-lambda'); const chromium = require('chrome-aws-lambda');
const middleware = require('./_common/middleware');
exports.handler = async (event, context, callback) => { const screenshotHandler = async (targetUrl) => {
let browser = null;
let targetUrl = event.queryStringParameters.url;
if (!targetUrl) { if (!targetUrl) {
callback(null, { throw new Error('URL is missing from queryStringParameters');
statusCode: 400,
body: JSON.stringify({ error: 'URL is missing from queryStringParameters' }),
});
return;
} }
if (!targetUrl.startsWith('http://') && !targetUrl.startsWith('https://')) { if (!targetUrl.startsWith('http://') && !targetUrl.startsWith('https://')) {
@@ -20,13 +15,10 @@ exports.handler = async (event, context, callback) => {
try { try {
new URL(targetUrl); new URL(targetUrl);
} catch (error) { } catch (error) {
callback(null, { throw new Error('URL provided is invalid');
statusCode: 400,
body: JSON.stringify({ error: 'URL provided is invalid' }),
});
return;
} }
let browser = null;
try { try {
browser = await puppeteer.launch({ browser = await puppeteer.launch({
args: chromium.args, args: chromium.args,
@@ -40,9 +32,7 @@ exports.handler = async (event, context, callback) => {
let page = await browser.newPage(); let page = await browser.newPage();
await page.emulateMediaFeatures([{ name: 'prefers-color-scheme', value: 'dark' }]); await page.emulateMediaFeatures([{ name: 'prefers-color-scheme', value: 'dark' }]);
page.setDefaultNavigationTimeout(8000); page.setDefaultNavigationTimeout(8000);
await page.goto(targetUrl, { waitUntil: 'domcontentloaded' }); await page.goto(targetUrl, { waitUntil: 'domcontentloaded' });
await page.evaluate(() => { await page.evaluate(() => {
@@ -57,24 +47,15 @@ exports.handler = async (event, context, callback) => {
}); });
const screenshotBuffer = await page.screenshot(); const screenshotBuffer = await page.screenshot();
const base64Screenshot = screenshotBuffer.toString('base64'); const base64Screenshot = screenshotBuffer.toString('base64');
const response = { return { image: base64Screenshot };
statusCode: 200,
body: JSON.stringify({ image: base64Screenshot }),
};
callback(null, response);
} catch (error) {
console.log(error);
callback(null, {
statusCode: 500,
body: JSON.stringify({ error: `An error occurred: ${error.message}` }),
});
} finally { } finally {
if (browser !== null) { if (browser !== null) {
await browser.close(); await browser.close();
} }
} }
}; };
exports.handler = middleware(screenshotHandler);

View File

@@ -1,5 +1,6 @@
const { https } = require('follow-redirects'); const { https } = require('follow-redirects');
const { URL } = require('url'); const { URL } = require('url');
const middleware = require('./_common/middleware');
const SECURITY_TXT_PATHS = [ const SECURITY_TXT_PATHS = [
'/security.txt', '/security.txt',
@@ -37,59 +38,39 @@ const isPgpSigned = (result) => {
return false; return false;
}; };
exports.handler = async (event, context) => { const securityTxtHandler = async (urlParam) => {
const urlParam = event.queryStringParameters.url;
if (!urlParam) {
return {
statusCode: 400,
body: JSON.stringify({ error: 'Missing url parameter' })
};
}
let url; let url;
try { try {
url = new URL(urlParam.includes('://') ? urlParam : 'https://' + urlParam); url = new URL(urlParam.includes('://') ? urlParam : 'https://' + urlParam);
} catch (error) { } catch (error) {
return { throw new Error('Invalid URL format');
statusCode: 500,
body: JSON.stringify({ error: 'Invalid URL format' }),
};
} }
url.pathname = ''; url.pathname = '';
for (let path of SECURITY_TXT_PATHS) { for (let path of SECURITY_TXT_PATHS) {
try { try {
const result = await fetchSecurityTxt(url, path); const result = await fetchSecurityTxt(url, path);
if (result && result.includes('<html')) return { if (result && result.includes('<html')) return { isPresent: false };
statusCode: 200,
body: JSON.stringify({ isPresent: false }),
};
if (result) { if (result) {
return { return {
statusCode: 200, isPresent: true,
body: JSON.stringify({ foundIn: path,
isPresent: true, content: result,
foundIn: path, isPgpSigned: isPgpSigned(result),
content: result, fields: parseResult(result),
isPgpSigned: isPgpSigned(result),
fields: parseResult(result),
}),
}; };
} }
} catch (error) { } catch (error) {
return { throw new Error(error.message);
statusCode: 500,
body: JSON.stringify({ error: error.message }),
};
} }
} }
return { return { isPresent: false };
statusCode: 404,
body: JSON.stringify({ isPresent: false }),
};
}; };
exports.handler = middleware(securityTxtHandler);
async function fetchSecurityTxt(baseURL, path) { async function fetchSecurityTxt(baseURL, path) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const url = new URL(path, baseURL); const url = new URL(path, baseURL);

View File

@@ -1,8 +1,9 @@
const commonMiddleware = require('./_common/middleware');
const axios = require('axios'); const axios = require('axios');
const xml2js = require('xml2js'); const xml2js = require('xml2js');
exports.handler = async (event) => { const handler = async (url) => {
const url = event.queryStringParameters.url;
let sitemapUrl = `${url}/sitemap.xml`; let sitemapUrl = `${url}/sitemap.xml`;
try { try {
@@ -59,3 +60,5 @@ exports.handler = async (event) => {
} }
} }
}; };
exports.handler = commonMiddleware(handler);

View File

@@ -1,8 +1,9 @@
const commonMiddleware = require('./_common/middleware');
const axios = require('axios'); const axios = require('axios');
const cheerio = require('cheerio'); const cheerio = require('cheerio');
exports.handler = async (event, context) => { const handler = async (url) => {
let url = event.queryStringParameters.url;
// Check if url includes protocol // Check if url includes protocol
if (!url.startsWith('http://') && !url.startsWith('https://')) { if (!url.startsWith('http://') && !url.startsWith('https://')) {
@@ -66,3 +67,5 @@ exports.handler = async (event, context) => {
}; };
} }
}; };
exports.handler = commonMiddleware(handler);

View File

@@ -1,50 +0,0 @@
const https = require('https');
exports.handler = async function (event, context) {
const { url } = event.queryStringParameters;
const errorResponse = (message, statusCode = 500) => {
return {
statusCode: statusCode,
body: JSON.stringify({ error: message }),
};
};
if (!url) {
return errorResponse('URL query parameter is required', 400);
}
try {
const response = await new Promise((resolve, reject) => {
const req = https.request(url, res => {
// Check if the SSL handshake was authorized
if (!res.socket.authorized) {
resolve(errorResponse(`SSL handshake not authorized. Reason: ${res.socket.authorizationError}`));
} else {
let cert = res.socket.getPeerCertificate(true);
if (!cert || Object.keys(cert).length === 0) {
resolve(errorResponse("No certificate presented by the server."));
} else {
// omit the raw and issuerCertificate fields
const { raw, issuerCertificate, ...certWithoutRaw } = cert;
resolve({
statusCode: 200,
body: JSON.stringify(certWithoutRaw),
});
}
}
});
req.on('error', error => {
resolve(errorResponse(`Error fetching site certificate: ${error.message}`));
});
req.end();
});
return response;
} catch (error) {
return errorResponse(`Unexpected error occurred: ${error.message}`);
}
};

43
api/ssl.js Normal file
View File

@@ -0,0 +1,43 @@
const tls = require('tls');
const middleware = require('./_common/middleware');
const fetchSiteCertificateHandler = async (urlString) => {
try {
const parsedUrl = new URL(urlString);
const options = {
host: parsedUrl.hostname,
port: parsedUrl.port || 443,
servername: parsedUrl.hostname,
rejectUnauthorized: false,
};
return new Promise((resolve, reject) => {
const socket = tls.connect(options, () => {
if (!socket.authorized) {
return reject(new Error(`SSL handshake not authorized. Reason: ${socket.authorizationError}`));
}
const cert = socket.getPeerCertificate();
if (!cert || Object.keys(cert).length === 0) {
return reject(new Error(`
No certificate presented by the server.\n
The server is possibly not using SNI (Server Name Indication) to identify itself, and you are connecting to a hostname-aliased IP address.
Or it may be due to an invalid SSL certificate, or an incomplete SSL handshake at the time the cert is being read.`));
}
const { raw, issuerCertificate, ...certWithoutRaw } = cert;
resolve(certWithoutRaw);
socket.end();
});
socket.on('error', (error) => {
reject(new Error(`Error fetching site certificate: ${error.message}`));
});
});
} catch (error) {
throw new Error(error.message);
}
};
exports.handler = middleware(fetchSiteCertificateHandler);

View File

@@ -1,14 +1,10 @@
const https = require('https'); const https = require('https');
const { performance, PerformanceObserver } = require('perf_hooks'); const { performance, PerformanceObserver } = require('perf_hooks');
const middleware = require('./_common/middleware');
exports.handler = async function(event, context) { const checkURLHandler = async (url) => {
const { url } = event.queryStringParameters;
if (!url) { if (!url) {
return { throw new Error('You must provide a URL query parameter!');
statusCode: 400,
body: JSON.stringify({ error: 'You must provide a URL query parameter!' }),
};
} }
let dnsLookupTime; let dnsLookupTime;
@@ -43,10 +39,7 @@ exports.handler = async function(event, context) {
}); });
if (responseCode < 200 || responseCode >= 400) { if (responseCode < 200 || responseCode >= 400) {
return { throw new Error(`Received non-success response code: ${responseCode}`);
statusCode: 200,
body: JSON.stringify({ error: `Received non-success response code: ${responseCode}` }),
};
} }
performance.mark('B'); performance.mark('B');
@@ -54,16 +47,12 @@ exports.handler = async function(event, context) {
let responseTime = performance.now() - startTime; let responseTime = performance.now() - startTime;
obs.disconnect(); obs.disconnect();
return { return { isUp: true, dnsLookupTime, responseTime, responseCode };
statusCode: 200,
body: JSON.stringify({ isUp: true, dnsLookupTime, responseTime, responseCode }),
};
} catch (error) { } catch (error) {
obs.disconnect(); obs.disconnect();
return { throw error;
statusCode: 200,
body: JSON.stringify({ error: `Error during operation: ${error.message}` }),
};
} }
}; };
exports.handler = middleware(checkURLHandler);

View File

@@ -1,69 +1,30 @@
const Wappalyzer = require('wappalyzer'); const Wappalyzer = require('wappalyzer');
const middleware = require('./_common/middleware');
const analyze = async (url) => { const analyzeSiteTechnologies = async (url) => {
const options = {}; const options = {};
const wappalyzer = new Wappalyzer(options); const wappalyzer = new Wappalyzer(options);
return (async function() {
try {
await wappalyzer.init()
const headers = {}
const storage = {
local: {},
session: {},
}
const site = await wappalyzer.open(url, headers, storage)
const results = await site.analyze()
return results;
} catch (error) {
return error;
} finally {
await wappalyzer.destroy()
}
})();
}
exports.handler = async (event, context, callback) => {
// Validate URL parameter
if (!event.queryStringParameters || !event.queryStringParameters.url) {
return {
statusCode: 400,
body: JSON.stringify({ error: 'Missing url parameter' }),
};
}
// Get URL from param
let url = event.queryStringParameters.url;
if (!/^https?:\/\//i.test(url)) {
url = 'http://' + url;
}
try { try {
return analyze(url).then( await wappalyzer.init();
(results) => { const headers = {};
if (!results.technologies || results.technologies.length === 0) { const storage = {
return { local: {},
statusCode: 200, session: {},
body: JSON.stringify({ error: 'Unable to find any technologies for site' }),
};
}
return {
statusCode: 200,
body: JSON.stringify(results),
}
}
)
.catch((error) => {
return {
statusCode: 500,
body: JSON.stringify({ error: error.message }),
};
});
} catch (error) {
return {
statusCode: 500,
body: JSON.stringify({ error: error.message }),
}; };
const site = await wappalyzer.open(url, headers, storage);
const results = await site.analyze();
if (!results.technologies || results.technologies.length === 0) {
throw new Error('Unable to find any technologies for site');
}
return results;
} catch (error) {
throw new Error(error.message);
} finally {
await wappalyzer.destroy();
} }
}; };
exports.handler = middleware(analyzeSiteTechnologies);

95
api/threats.js Normal file
View File

@@ -0,0 +1,95 @@
const axios = require('axios');
const xml2js = require('xml2js');
const middleware = require('./_common/middleware');
const getGoogleSafeBrowsingResult = async (url) => {
try {
const apiKey = process.env.GOOGLE_CLOUD_API_KEY;
const apiEndpoint = `https://safebrowsing.googleapis.com/v4/threatMatches:find?key=${apiKey}`;
const requestBody = {
threatInfo: {
threatTypes: [
'MALWARE', 'SOCIAL_ENGINEERING', 'UNWANTED_SOFTWARE', 'POTENTIALLY_HARMFUL_APPLICATION', 'API_ABUSE'
],
platformTypes: ["ANY_PLATFORM"],
threatEntryTypes: ["URL"],
threatEntries: [{ url }]
}
};
const response = await axios.post(apiEndpoint, requestBody);
if (response.data && response.data.matches) {
return {
unsafe: true,
details: response.data.matches
};
} else {
return { unsafe: false };
}
} catch (error) {
return { error: `Request failed: ${error.message}` };
}
};
const getUrlHausResult = async (url) => {
let domain = new URL(url).hostname;
return await axios({
method: 'post',
url: 'https://urlhaus-api.abuse.ch/v1/host/',
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
data: `host=${domain}`
})
.then((x) => x.data)
.catch((e) => ({ error: `Request to URLHaus failed, ${e.message}`}));
};
const getPhishTankResult = async (url) => {
try {
const encodedUrl = Buffer.from(url).toString('base64');
const endpoint = `https://checkurl.phishtank.com/checkurl/?url=${encodedUrl}`;
const headers = {
'User-Agent': 'phishtank/web-check',
};
const response = await axios.post(endpoint, null, { headers, timeout: 3000 });
const parsed = await xml2js.parseStringPromise(response.data, { explicitArray: false });
return parsed.response.results;
} catch (error) {
return { error: `Request to PhishTank failed: ${error.message}` };
}
}
const getCloudmersiveResult = async (url) => {
try {
const endpoint = 'https://api.cloudmersive.com/virus/scan/website';
const headers = {
'Content-Type': 'application/x-www-form-urlencoded',
'Apikey': process.env.CLOUDMERSIVE_API_KEY,
};
const data = `Url=${encodeURIComponent(url)}`;
const response = await axios.post(endpoint, data, { headers });
return response.data;
} catch (error) {
return { error: `Request to Cloudmersive failed: ${error.message}` };
}
};
const handler = async (url) => {
try {
const urlHaus = await getUrlHausResult(url);
const phishTank = await getPhishTankResult(url);
const cloudmersive = await getCloudmersiveResult(url);
const safeBrowsing = await getGoogleSafeBrowsingResult(url);
if (urlHaus.error && phishTank.error && cloudmersive.error && safeBrowsing.error) {
throw new Error(`All requests failed - ${urlHaus.error} ${phishTank.error} ${cloudmersive.error} ${safeBrowsing.error}`);
}
return JSON.stringify({ urlHaus, phishTank, cloudmersive, safeBrowsing });
} catch (error) {
throw new Error(error.message);
}
};
exports.handler = middleware(handler);

28
api/tls.js Normal file
View File

@@ -0,0 +1,28 @@
const axios = require('axios');
const middleware = require('./_common/middleware');
const MOZILLA_TLS_OBSERVATORY_API = 'https://tls-observatory.services.mozilla.com/api/v1';
const handler = async (url) => {
try {
const domain = new URL(url).hostname;
const scanResponse = await axios.post(`${MOZILLA_TLS_OBSERVATORY_API}/scan?target=${domain}`);
const scanId = scanResponse.data.scan_id;
if (typeof scanId !== 'number') {
return {
statusCode: 500,
body: JSON.stringify({ error: 'Failed to get scan_id from TLS Observatory' }),
};
}
const resultResponse = await axios.get(`${MOZILLA_TLS_OBSERVATORY_API}/results?id=${scanId}`);
return {
statusCode: 200,
body: JSON.stringify(resultResponse.data),
};
} catch (error) {
return { error: error.message };
}
};
exports.handler = middleware(handler);

View File

@@ -1,55 +1,31 @@
const traceroute = require('traceroute'); const traceroute = require('traceroute');
const url = require('url'); const url = require('url');
const middleware = require('./_common/middleware');
exports.handler = async function(event, context) { const executeTraceroute = async (urlString, context) => {
const urlString = event.queryStringParameters.url; // Parse the URL and get the hostname
const urlObject = url.parse(urlString);
const host = urlObject.hostname;
try { if (!host) {
if (!urlString) { throw new Error('Invalid URL provided');
throw new Error('URL parameter is missing!'); }
}
// Parse the URL and get the hostname // Traceroute with callback
const urlObject = url.parse(urlString); const result = await new Promise((resolve, reject) => {
const host = urlObject.hostname; traceroute.trace(host, (err, hops) => {
if (err || !hops) {
if (!host) { reject(err || new Error('No hops found'));
throw new Error('Invalid URL provided'); } else {
} resolve(hops);
// Traceroute with callback
const result = await new Promise((resolve, reject) => {
traceroute.trace(host, (err, hops) => {
if (err || !hops) {
reject(err || new Error('No hops found'));
} else {
resolve(hops);
}
});
// Check if remaining time is less than 8.8 seconds, then reject promise
if (context.getRemainingTimeInMillis() < 8800) {
reject(new Error('Lambda is about to timeout'));
} }
}); });
});
return { return {
statusCode: 200, message: "Traceroute completed!",
body: JSON.stringify({ result,
message: "Traceroute completed!", };
result,
}),
};
} catch (err) {
const message = err.code === 'ENOENT'
? 'Traceroute command is not installed on the host.'
: err.message;
return {
statusCode: 500,
body: JSON.stringify({
error: message,
}),
};
}
}; };
exports.handler = middleware(executeTraceroute);

View File

@@ -1,9 +1,11 @@
const dns = require('dns').promises; const dns = require('dns').promises;
const middleware = require('./_common/middleware');
exports.handler = async (event) => { const handler = async (url, event, context) => {
const url = new URL(event.queryStringParameters.url);
try { try {
const txtRecords = await dns.resolveTxt(url.hostname); const parsedUrl = new URL(url);
const txtRecords = await dns.resolveTxt(parsedUrl.hostname);
// Parsing and formatting TXT records into a single object // Parsing and formatting TXT records into a single object
const readableTxtRecords = txtRecords.reduce((acc, recordArray) => { const readableTxtRecords = txtRecords.reduce((acc, recordArray) => {
@@ -16,15 +18,15 @@ exports.handler = async (event) => {
return { ...acc, ...recordObject }; return { ...acc, ...recordObject };
}, {}); }, {});
return { return readableTxtRecords;
statusCode: 200,
body: JSON.stringify(readableTxtRecords),
};
} catch (error) { } catch (error) {
console.error('Error:', error); if (error.code === 'ERR_INVALID_URL') {
return { throw new Error(`Invalid URL ${error}`);
statusCode: 500, } else {
body: JSON.stringify({ error: error.message }), throw error;
}; }
} }
}; };
exports.handler = middleware(handler);

View File

@@ -1,14 +1,7 @@
const net = require('net'); const net = require('net');
const psl = require('psl'); const psl = require('psl');
// const { URL } = require('url'); const axios = require('axios');
const middleware = require('./_common/middleware');
const errorResponse = (message, statusCode = 444) => {
return {
statusCode: statusCode,
body: JSON.stringify({ error: message }),
};
};
const getBaseDomain = (url) => { const getBaseDomain = (url) => {
let protocol = ''; let protocol = '';
@@ -22,55 +15,7 @@ const getBaseDomain = (url) => {
return protocol + parsed.domain; return protocol + parsed.domain;
}; };
exports.handler = async function(event, context) {
let url = event.queryStringParameters.url;
if (!url) {
return errorResponse('URL query parameter is required.', 400);
}
if (!url.startsWith('http://') && !url.startsWith('https://')) {
url = 'http://' + url;
}
let hostname;
try {
hostname = getBaseDomain(new URL(url).hostname);
} catch (error) {
return errorResponse(`Unable to parse URL: ${error}`, 400);
}
return new Promise((resolve, reject) => {
const client = net.createConnection({ port: 43, host: 'whois.internic.net' }, () => {
client.write(hostname + '\r\n');
});
let data = '';
client.on('data', (chunk) => {
data += chunk;
});
client.on('end', () => {
try {
const parsedData = parseWhoisData(data);
resolve({
statusCode: 200,
body: JSON.stringify(parsedData),
});
} catch (error) {
resolve(errorResponse(error.message));
}
});
client.on('error', (err) => {
resolve(errorResponse(err.message, 500));
});
});
};
const parseWhoisData = (data) => { const parseWhoisData = (data) => {
if (data.includes('No match for')) { if (data.includes('No match for')) {
return { error: 'No matches found for domain in internic database'}; return { error: 'No matches found for domain in internic database'};
} }
@@ -100,3 +45,65 @@ const parseWhoisData = (data) => {
return parsedData; return parsedData;
}; };
const fetchFromInternic = async (hostname) => {
return new Promise((resolve, reject) => {
const client = net.createConnection({ port: 43, host: 'whois.internic.net' }, () => {
client.write(hostname + '\r\n');
});
let data = '';
client.on('data', (chunk) => {
data += chunk;
});
client.on('end', () => {
try {
const parsedData = parseWhoisData(data);
resolve(parsedData);
} catch (error) {
reject(error);
}
});
client.on('error', (err) => {
reject(err);
});
});
};
const fetchFromMyAPI = async (hostname) => {
try {
const response = await axios.post('https://whois-api-zeta.vercel.app/', {
domain: hostname
});
return response.data;
} catch (error) {
console.error('Error fetching data from your API:', error.message);
return null;
}
};
const fetchWhoisData = async (url) => {
if (!url.startsWith('http://') && !url.startsWith('https://')) {
url = 'http://' + url;
}
let hostname;
try {
hostname = getBaseDomain(new URL(url).hostname);
} catch (error) {
throw new Error(`Unable to parse URL: ${error}`);
}
const [internicData, whoisData] = await Promise.all([
fetchFromInternic(hostname),
fetchFromMyAPI(hostname)
]);
return {
internicData,
whoisData
};
};
exports.handler = middleware(fetchWhoisData);

View File

@@ -4,5 +4,5 @@ services:
container_name: Web-Check container_name: Web-Check
image: lissy93/web-check image: lissy93/web-check
ports: ports:
- 8888:8888 - 3000:3000
restart: unless-stopped restart: unless-stopped

View File

@@ -16,7 +16,7 @@
}, },
"scripts": { "scripts": {
"dev": "netlify dev", "dev": "netlify dev",
"serve": "netlify serve --offline", "serve": "node server",
"start": "react-scripts start", "start": "react-scripts start",
"build": "react-scripts build", "build": "react-scripts build",
"test": "react-scripts test", "test": "react-scripts test",
@@ -35,9 +35,14 @@
"@types/react-router-dom": "^5.3.3", "@types/react-router-dom": "^5.3.3",
"@types/react-simple-maps": "^3.0.0", "@types/react-simple-maps": "^3.0.0",
"@types/styled-components": "^5.1.26", "@types/styled-components": "^5.1.26",
"aws-serverless-express": "^3.4.0",
"axios": "^1.4.0", "axios": "^1.4.0",
"cheerio": "^1.0.0-rc.12", "cheerio": "^1.0.0-rc.12",
"chrome-aws-lambda": "^10.1.0", "chrome-aws-lambda": "^10.1.0",
"chromium": "^3.0.3",
"connect-history-api-fallback": "^2.0.0",
"csv-parser": "^3.0.0",
"dotenv": "^16.3.1",
"flatted": "^3.2.7", "flatted": "^3.2.7",
"follow-redirects": "^1.15.2", "follow-redirects": "^1.15.2",
"got": "^13.0.0", "got": "^13.0.0",
@@ -46,7 +51,7 @@
"perf_hooks": "^0.0.1", "perf_hooks": "^0.0.1",
"psl": "^1.9.0", "psl": "^1.9.0",
"puppeteer": "^20.9.0", "puppeteer": "^20.9.0",
"puppeteer-core": "^20.9.0", "puppeteer-core": "^21.0.3",
"react": "^18.2.0", "react": "^18.2.0",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"react-masonry-css": "^1.0.16", "react-masonry-css": "^1.0.16",
@@ -54,12 +59,14 @@
"react-scripts": "5.0.1", "react-scripts": "5.0.1",
"react-simple-maps": "^3.0.0", "react-simple-maps": "^3.0.0",
"react-toastify": "^9.1.3", "react-toastify": "^9.1.3",
"recharts": "^2.7.3",
"styled-components": "^6.0.5", "styled-components": "^6.0.5",
"traceroute": "^1.0.0", "traceroute": "^1.0.0",
"typescript": "^5.1.6", "typescript": "^5.1.6",
"wappalyzer": "^6.10.63", "unzipper": "^0.10.14",
"wappalyzer": "^6.10.65",
"web-vitals": "^3.4.0", "web-vitals": "^3.4.0",
"xml2js": "^0.6.0" "xml2js": "^0.6.2"
}, },
"eslintConfig": { "eslintConfig": {
"extends": [ "extends": [
@@ -82,5 +89,12 @@
"compilerOptions": { "compilerOptions": {
"allowJs": true, "allowJs": true,
"outDir": "./dist" "outDir": "./dist"
},
"devDependencies": {
"serverless-domain-manager": "^7.1.1",
"serverless-offline": "^12.0.4",
"serverless-webpack": "^5.13.0",
"webpack": "^5.88.2",
"webpack-node-externals": "^3.0.0"
} }
} }

BIN
public/banner.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 272 KiB

View File

@@ -7,15 +7,29 @@
<meta name="theme-color" content="#9fef00" /> <meta name="theme-color" content="#9fef00" />
<meta <meta
name="description" name="description"
content="Web site created using create-react-app" content="All-in-one OSINT tool, for quickly checking a websites data"
/> />
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" /> <link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" /> <link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<title>Web Check</title> <title>Web Check</title>
<script defer data-domain="web-check.as93.net" src="https://no-track.as93.net/js/script.js"></script> <script defer data-domain="web-check.as93.net" src="https://no-track.as93.net/js/script.js"></script>
<!-- OpenGraph Social Tags -->
<meta property="og:title" content="Web Check">
<meta property="og:description" content="All-in-one Website OSINT Scanner">
<meta property="og:image" content="/banner.png">
<meta property="og:url" content="https://web-check.as93.net">
<meta name="twitter:card" content="summary_large_image">
</head> </head>
<body> <body>
<noscript>All-in-one OSINT tool, for quickly checking a websites data</noscript> <noscript>
<b>Welcome to Web-Check, the free and open source tool for viewing all available information about a website.</b><br />
Get started by entering a URL, and clicking the "Scan" button, or view the code and docs
on <a href="https://github.com/lissy93/web-check">GitHub</a>.<br />
<small>Licensed under MIT, ©️ <a href="https://aliciasykes.com">Alicia Sykes</a> 2023.</small>
<br /><br />
JavaScript is required to continue, please enable it in your browser.
</noscript>
<div id="root"></div> <div id="root"></div>
</body> </body>
</html> </html>

116
server.js Normal file
View File

@@ -0,0 +1,116 @@
const express = require('express');
const awsServerlessExpress = require('aws-serverless-express');
const fs = require('fs');
const path = require('path');
const historyApiFallback = require('connect-history-api-fallback');
require('dotenv').config();
const app = express();
const API_DIR = '/api'; // Name of the dir containing the lambda functions
const dirPath = path.join(__dirname, API_DIR); // Path to the lambda functions dir
const guiPath = path.join(__dirname, 'build');
// Execute the lambda function
const executeHandler = async (handler, req) => {
return new Promise((resolve, reject) => {
const callback = (err, response) => err ? reject(err) : resolve(response);
const promise = handler(req, {}, callback);
if (promise && typeof promise.then === 'function') {
promise.then(resolve).catch(reject);
}
});
};
// Array of all the lambda function file names
const fileNames = fs.readdirSync(dirPath, { withFileTypes: true })
.filter(dirent => dirent.isFile() && dirent.name.endsWith('.js'))
.map(dirent => dirent.name);
const handlers = {};
fileNames.forEach(file => {
const routeName = file.split('.')[0];
const route = `${API_DIR}/${routeName}`;
const handler = require(path.join(dirPath, file)).handler;
handlers[route] = handler;
app.get(route, async (req, res) => {
try {
const { statusCode = 200, body = '' } = await executeHandler(handler, req);
res.status(statusCode).json(JSON.parse(body));
} catch (err) {
res.status(500).json({ error: err.message });
}
});
});
const timeout = (ms, jobName = null) => {
return new Promise((_, reject) => {
setTimeout(() => {
reject(new Error(`Timed out after the ${ms/1000} second limit${jobName ? `, when executing the ${jobName} task` : ''}`));
}, ms);
});
}
app.get('/api', async (req, res) => {
const results = {};
const { url } = req.query;
const maxExecutionTime = process.env.API_TIMEOUT_LIMIT || 15000;
const handlerPromises = Object.entries(handlers).map(async ([route, handler]) => {
const routeName = route.replace(`${API_DIR}/`, '');
try {
const result = await Promise.race([
executeHandler(handler, { query: { url } }),
timeout(maxExecutionTime, routeName)
]);
results[routeName] = JSON.parse((result || {}).body);
} catch (err) {
results[routeName] = { error: err.message };
}
});
await Promise.all(handlerPromises);
res.json(results);
});
// Handle SPA routing
app.use(historyApiFallback({
rewrites: [
{ from: /^\/api\/.*$/, to: function(context) { return context.parsedUrl.path; } },
]
}));
// Serve up the GUI - if build dir exists, and GUI feature enabled
if (process.env.DISABLE_GUI && process.env.DISABLE_GUI !== 'false') {
app.get('*', (req, res) => {
res.status(500).send(
'Welcome to Web-Check!<br />Access the API endpoints at '
+'<a href="/api"><code>/api</code></a>'
);
});
} else if (!fs.existsSync(guiPath)) {
app.get('*', (req, res) => {
res.status(500).send(
'Welcome to Web-Check!<br />Looks like the GUI app has not yet been compiled, '
+'run <code>yarn build</code> to continue, then restart the server.'
);
});
} else { // GUI enabled, and build files present, let's go!!
app.use(express.static(guiPath));
}
// Create serverless express server
const port = process.env.PORT || 3000;
const server = awsServerlessExpress.createServer(app).listen(port, () => {
console.log(`Server is running on port ${port}`);
});
exports.handler = (event, context) => {
awsServerlessExpress.proxy(server, event, context);
};

179
serverless.yml Normal file
View File

@@ -0,0 +1,179 @@
service: web-check-api
provider:
name: aws
runtime: nodejs14.x
region: us-east-1
functions:
dnssec:
handler: api/dnssec.handler
events:
- http:
path: api/dnssec
method: get
linkedPages:
handler: api/linked-pages.handler
events:
- http:
path: api/linked-pages
method: get
robotsTxt:
handler: api/robots-txt.handler
events:
- http:
path: api/robots-txt
method: get
ssl:
handler: api/ssl.handler
events:
- http:
path: api/ssl
method: get
whois:
handler: api/whois.handler
events:
- http:
path: api/whois
method: get
carbon:
handler: api/carbon.handler
events:
- http:
path: api/carbon
method: get
features:
handler: api/features.handler
events:
- http:
path: api/features
method: get
mailConfig:
handler: api/mail-config.handler
events:
- http:
path: api/mail-config
method: get
screenshot:
handler: api/screenshot.handler
events:
- http:
path: api/screenshot
method: get
status:
handler: api/status.handler
events:
- http:
path: api/status
method: get
cookies:
handler: api/cookies.handler
events:
- http:
path: api/cookies
method: get
getIp:
handler: api/get-ip.handler
events:
- http:
path: api/get-ip
method: get
ports:
handler: api/ports.handler
events:
- http:
path: api/ports
method: get
securityTxt:
handler: api/security-txt.handler
events:
- http:
path: api/security-txt
method: get
techStack:
handler: api/tech-stack.handler
events:
- http:
path: api/tech-stack
method: get
dnsServer:
handler: api/dns-server.handler
events:
- http:
path: api/dns-server
method: get
headers:
handler: api/headers.handler
events:
- http:
path: api/headers
method: get
quality:
handler: api/quality.handler
events:
- http:
path: api/quality
method: get
sitemap:
handler: api/sitemap.handler
events:
- http:
path: api/sitemap
method: get
traceRoute:
handler: api/trace-route.handler
events:
- http:
path: api/trace-route
method: get
dns:
handler: api/dns.handler
events:
- http:
path: api/dns
method: get
hsts:
handler: api/hsts.handler
events:
- http:
path: api/hsts
method: get
redirects:
handler: api/redirects.handler
events:
- http:
path: api/redirects
method: get
socialTags:
handler: api/social-tags.handler
events:
- http:
path: api/social-tags
method: get
txtRecords:
handler: api/txt-records.handler
events:
- http:
path: api/txt-records
method: get
plugins:
# - serverless-webpack
- serverless-domain-manager
- serverless-offline
custom:
webpack:
webpackConfig: 'api/_common/aws-webpack.config.js'
includeModules: true
customDomain:
domainName: example.com
basePath: 'api'
stage: ${self:provider.stage}
createRoute53Record: true
serverless-offline:
prefix: ''
httpPort: 3000

View File

@@ -1,7 +1,9 @@
import styled from 'styled-components'; import styled, { keyframes } from 'styled-components';
import colors from 'styles/colors'; import colors from 'styles/colors';
import { InputSize, applySize } from 'styles/dimensions'; import { InputSize, applySize } from 'styles/dimensions';
type LoadState = 'loading' | 'success' | 'error';
interface ButtonProps { interface ButtonProps {
children: React.ReactNode; children: React.ReactNode;
onClick?: React.MouseEventHandler<HTMLButtonElement>; onClick?: React.MouseEventHandler<HTMLButtonElement>;
@@ -10,6 +12,7 @@ interface ButtonProps {
fgColor?: string, fgColor?: string,
styles?: string, styles?: string,
title?: string, title?: string,
loadState?: LoadState,
}; };
const StyledButton = styled.button<ButtonProps>` const StyledButton = styled.button<ButtonProps>`
@@ -19,6 +22,9 @@ const StyledButton = styled.button<ButtonProps>`
font-family: PTMono; font-family: PTMono;
box-sizing: border-box; box-sizing: border-box;
width: -moz-available; width: -moz-available;
display: flex;
justify-content: center;
gap: 1rem;
box-shadow: 3px 3px 0px ${colors.fgShadowColor}; box-shadow: 3px 3px 0px ${colors.fgShadowColor};
&:hover { &:hover {
box-shadow: 5px 5px 0px ${colors.fgShadowColor}; box-shadow: 5px 5px 0px ${colors.fgShadowColor};
@@ -36,8 +42,29 @@ const StyledButton = styled.button<ButtonProps>`
${props => props.styles} ${props => props.styles}
`; `;
const spinAnimation = keyframes`
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
`;
const SimpleLoader = styled.div`
border: 4px solid rgba(255, 255, 255, 0.3);
border-radius: 50%;
border-top: 4px solid ${colors.background};
width: 1rem;
height: 1rem;
animation: ${spinAnimation} 1s linear infinite;
`;
const Loader = (props: { loadState: LoadState }) => {
if (props.loadState === 'loading') return <SimpleLoader />
if (props.loadState === 'success') return <span></span>
if (props.loadState === 'error') return <span></span>
return <span></span>;
};
const Button = (props: ButtonProps): JSX.Element => { const Button = (props: ButtonProps): JSX.Element => {
const { children, size, bgColor, fgColor, onClick, styles, title } = props; const { children, size, bgColor, fgColor, onClick, styles, title, loadState } = props;
return ( return (
<StyledButton <StyledButton
onClick={onClick || (() => null) } onClick={onClick || (() => null) }
@@ -47,6 +74,7 @@ const Button = (props: ButtonProps): JSX.Element => {
styles={styles} styles={styles}
title={title?.toString()} title={title?.toString()}
> >
{ loadState && <Loader loadState={loadState} /> }
{children} {children}
</StyledButton> </StyledButton>
); );

Some files were not shown because too many files have changed in this diff Show More