Skip to content

Commit 3925ae9

Browse files
gl0bal01claude
andcommitted
fix(security): address all Docker security audit findings
HIGH fixes: - F10: Stop leaking secrets to child processes — safeSpawn now uses minimal env (PATH, HOME, LANG only) instead of full process.env - F1: Pin Dockerfile base image to node:18.20-slim - F2: Multi-stage build — builder stage for npm ci, clean runtime stage - F9: Create docker-compose.yml with no-new-privileges, cap_drop ALL, read_only fs, tmpfs mounts, memory/PID limits - F12: Lazy-init AWS Rekognition client with credential validation MEDIUM fixes: - F15: Add getSafeAxiosConfig to rekognition.js downloads - F4: Change default tool paths from /root/ to /opt/ (non-root accessible) - F25: Move JWT temp folder inside /app/temp/ - F26: Move ghunt results inside /app/temp/ - F23: Add 50MB output file size limit to safeSpawnToFile - F17: Add Trivy image scanning to CI pipeline - F18: Pin GitHub Actions to SHA commits - F21: Tighten npm audit to --audit-level=moderate LOW fixes: - F8: Expand .dockerignore (admin scripts, CI configs, docs) - F24: Add 1MB stderr buffer cap to safeSpawn/safeSpawnToFile - F27: Add startup cleanup for orphaned temp files >24h old 45 tests passing, 0 lint errors. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 04a3afe commit 3925ae9

File tree

12 files changed

+157
-48
lines changed

12 files changed

+157
-48
lines changed

.dockerignore

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,13 @@ docs/
77
*.md
88
.eslintrc*
99
.gitignore
10+
.github/
11+
eslint.config.js
12+
ghunt_results/
13+
*.test.js
14+
vitest.config.*
15+
CONTRIBUTING.md
16+
SECURITY.md
17+
REPOSITORY_SUMMARY.md
18+
CITATION.cff
19+
zenodo.json

.github/workflows/ci.yml

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,9 @@ jobs:
1313
matrix:
1414
node-version: [18, 20, 22]
1515
steps:
16-
- uses: actions/checkout@v4
16+
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
1717
- name: Use Node.js ${{ matrix.node-version }}
18-
uses: actions/setup-node@v4
18+
uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0
1919
with:
2020
node-version: ${{ matrix.node-version }}
2121
- run: npm ci
@@ -25,17 +25,23 @@ jobs:
2525
audit:
2626
runs-on: ubuntu-latest
2727
steps:
28-
- uses: actions/checkout@v4
29-
- uses: actions/setup-node@v4
28+
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
29+
- uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0
3030
with:
3131
node-version: 18
3232
- run: npm ci
33-
- run: npm audit --audit-level=high
33+
- run: npm audit --audit-level=moderate
3434

3535
docker:
3636
runs-on: ubuntu-latest
3737
needs: [test]
3838
steps:
39-
- uses: actions/checkout@v4
39+
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
4040
- name: Build Docker image
4141
run: docker build -t discord-osint-assistant .
42+
- name: Scan Docker image
43+
uses: aquasecurity/trivy-action@18f2510ee396bbf400402947e7f9de1e6e321aee # v0.29.0
44+
with:
45+
image-ref: discord-osint-assistant
46+
severity: CRITICAL,HIGH
47+
exit-code: 1

Dockerfile

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,21 @@
1-
FROM node:18-slim
2-
3-
# Create app directory
1+
# Pin to specific version for reproducible builds
2+
# Update periodically: docker pull node:18.20-slim
3+
FROM node:18.20-slim AS builder
44
WORKDIR /app
5-
6-
# Install only production dependencies (reproducible via lockfile)
75
COPY package.json package-lock.json ./
86
RUN npm ci --omit=dev
97

10-
# Copy application code
11-
COPY . .
8+
# Stage 2: Production runtime
9+
FROM node:18.20-slim
10+
RUN apt-get update && apt-get install -y --no-install-recommends procps && rm -rf /var/lib/apt/lists/*
11+
WORKDIR /app
12+
13+
# Copy only what's needed
14+
COPY --from=builder /app/node_modules ./node_modules
15+
COPY package.json index.js deploy-commands.js clear-commands.js ./
16+
COPY commands/ ./commands/
17+
COPY utils/ ./utils/
18+
COPY addons/ ./addons/
1219

1320
# Create temp directory
1421
RUN mkdir -p /app/temp
@@ -18,7 +25,6 @@ RUN groupadd -r botuser && useradd -r -g botuser botuser
1825
RUN chown -R botuser:botuser /app
1926
USER botuser
2027

21-
# Health check: verify the main node process is running
2228
HEALTHCHECK --interval=30s --timeout=10s --start-period=60s --retries=3 \
2329
CMD pgrep -f "node index.js" > /dev/null || exit 1
2430

commands/ghunt.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -326,7 +326,7 @@ module.exports = {
326326
const userTag = interaction.user.tag || username;
327327

328328
// Directory for results
329-
const resultsDir = path.join(__dirname, '../ghunt_results');
329+
const resultsDir = path.join(__dirname, '../temp/ghunt_results');
330330
if (!fs.existsSync(resultsDir)) {
331331
fs.mkdirSync(resultsDir, { recursive: true });
332332
}

commands/jwt.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ const crypto = require('crypto');
4242
// Configuration - adjust these paths according to your environment
4343
const CONFIG = {
4444
JWT_TOOL_PATH: process.env.JWT_TOOL_PATH || '/opt/tools/jwt_tool',
45-
TEMP_FOLDER: process.env.JWT_TEMP_FOLDER || '/tmp/jwt_analysis',
45+
TEMP_FOLDER: process.env.JWT_TEMP_FOLDER || path.join(__dirname, '..', 'temp', 'jwt_analysis'),
4646
DEFAULT_WORDLIST: process.env.JWT_WORDLIST || '/opt/rockyou.txt',
4747
COMMAND_TIMEOUT: parseInt(process.env.JWT_TIMEOUT) || 120000, // 2 minutes
4848
MAX_OUTPUT_SIZE: 10 * 1024 * 1024, // 10MB max output file size

commands/nuclei.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -163,7 +163,7 @@ module.exports = {
163163

164164
// Construct Nuclei args array (no shell interpolation)
165165
const nucleiBinary = process.env.NUCLEI_PATH || 'nuclei';
166-
const templatesPath = process.env.NUCLEI_TEMPLATE_PATH || '/root/nuclei-templates/http/osint/user-enumeration';
166+
const templatesPath = process.env.NUCLEI_TEMPLATE_PATH || '/opt/nuclei-templates/http/osint/user-enumeration';
167167
const args = [
168168
'-t', templatesPath,
169169
'-tags', tagsList.join(','),

commands/rekognition.js

Lines changed: 36 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010

1111
const { SlashCommandBuilder, EmbedBuilder, AttachmentBuilder } = require('discord.js');
1212
const axios = require('axios');
13-
const { validateUrlNotInternal } = require('../utils/ssrf');
13+
const { validateUrlNotInternal, getSafeAxiosConfig } = require('../utils/ssrf');
1414
const { isValidUrl } = require('../utils/validation');
1515
const fs = require('fs');
1616
const path = require('path');
@@ -21,14 +21,24 @@ const { URL } = require('url');
2121
const { RekognitionClient, DetectLabelsCommand, DetectTextCommand,
2222
DetectFacesCommand, DetectModerationLabelsCommand,
2323
RecognizeCelebritiesCommand, CompareFacesCommand } = require('@aws-sdk/client-rekognition');
24-
// Initialize the AWS Rekognition client (v3)
25-
const rekognitionClient = new RekognitionClient({
26-
region: process.env.AWS_REGION || 'us-east-1',
27-
credentials: {
28-
accessKeyId: process.env.AWS_ACCESS_KEY_ID,
29-
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY
24+
// Lazy-initialized AWS Rekognition client (v3)
25+
let _rekognitionClient = null;
26+
27+
function getRekognitionClient() {
28+
if (!_rekognitionClient) {
29+
if (!process.env.AWS_ACCESS_KEY_ID || !process.env.AWS_SECRET_ACCESS_KEY) {
30+
throw new Error('AWS credentials not configured. Set AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY.');
31+
}
32+
_rekognitionClient = new RekognitionClient({
33+
region: process.env.AWS_REGION || 'us-east-1',
34+
credentials: {
35+
accessKeyId: process.env.AWS_ACCESS_KEY_ID,
36+
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY
37+
}
38+
});
3039
}
31-
});
40+
return _rekognitionClient;
41+
}
3242

3343
module.exports = {
3444
data: new SlashCommandBuilder()
@@ -194,9 +204,10 @@ async function handleAnalyze(interaction, tempDir) {
194204

195205
try {
196206
// Download the attachment
197-
const response = await axios.get(uploadedImage.url, {
207+
const response = await axios.get(uploadedImage.url, {
198208
responseType: 'arraybuffer',
199-
timeout: 10000 // 10 second timeout
209+
timeout: 10000, // 10 second timeout
210+
...getSafeAxiosConfig()
200211
});
201212

202213
imageBuffer = Buffer.from(response.data);
@@ -439,9 +450,10 @@ async function handleCompare(interaction, tempDir) {
439450

440451
try {
441452
// Download the attachment
442-
const response = await axios.get(sourceAttachment.url, {
453+
const response = await axios.get(sourceAttachment.url, {
443454
responseType: 'arraybuffer',
444-
timeout: 10000
455+
timeout: 10000,
456+
...getSafeAxiosConfig()
445457
});
446458

447459
sourceImageBuffer = Buffer.from(response.data);
@@ -495,9 +507,10 @@ async function handleCompare(interaction, tempDir) {
495507

496508
try {
497509
// Download the attachment
498-
const response = await axios.get(targetAttachment.url, {
510+
const response = await axios.get(targetAttachment.url, {
499511
responseType: 'arraybuffer',
500-
timeout: 10000
512+
timeout: 10000,
513+
...getSafeAxiosConfig()
501514
});
502515

503516
targetImageBuffer = Buffer.from(response.data);
@@ -651,9 +664,10 @@ async function downloadImage(url, tempDir, prefix = '') {
651664
const filePath = path.join(tempDir, fileName);
652665

653666
// Download the image
654-
const response = await axios.get(url, {
667+
const response = await axios.get(url, {
655668
responseType: 'arraybuffer',
656-
timeout: 10000 // 10 second timeout
669+
timeout: 10000, // 10 second timeout
670+
...getSafeAxiosConfig()
657671
});
658672

659673
// Check if the response is an image
@@ -720,7 +734,7 @@ async function detectLabels(imageBuffer) {
720734
};
721735

722736
const command = new DetectLabelsCommand(params);
723-
const response = await rekognitionClient.send(command);
737+
const response = await getRekognitionClient().send(command);
724738
return response;
725739
}
726740

@@ -737,7 +751,7 @@ async function detectText(imageBuffer) {
737751
};
738752

739753
const command = new DetectTextCommand(params);
740-
const response = await rekognitionClient.send(command);
754+
const response = await getRekognitionClient().send(command);
741755
return response;
742756
}
743757

@@ -755,7 +769,7 @@ async function detectFaces(imageBuffer) {
755769
};
756770

757771
const command = new DetectFacesCommand(params);
758-
const response = await rekognitionClient.send(command);
772+
const response = await getRekognitionClient().send(command);
759773
return response;
760774
}
761775

@@ -773,7 +787,7 @@ async function detectModerationLabels(imageBuffer) {
773787
};
774788

775789
const command = new DetectModerationLabelsCommand(params);
776-
const response = await rekognitionClient.send(command);
790+
const response = await getRekognitionClient().send(command);
777791
return response;
778792
}
779793

@@ -790,7 +804,7 @@ async function recognizeCelebrities(imageBuffer) {
790804
};
791805

792806
const command = new RecognizeCelebritiesCommand(params);
793-
const response = await rekognitionClient.send(command);
807+
const response = await getRekognitionClient().send(command);
794808
return response;
795809
}
796810

@@ -813,6 +827,6 @@ async function compareFaces(sourceImageBuffer, targetImageBuffer, similarityThre
813827
};
814828

815829
const command = new CompareFacesCommand(params);
816-
const response = await rekognitionClient.send(command);
830+
const response = await getRekognitionClient().send(command);
817831
return response;
818832
}

commands/sherlock.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
*/
2626

2727
const { SlashCommandBuilder, AttachmentBuilder } = require('discord.js');
28-
const { safeSpawnToFile } = require('../utils/process');
28+
const { safeSpawnToFile, getSafeEnv } = require('../utils/process');
2929
const fs = require('fs').promises;
3030
const path = require('path');
3131
const crypto = require('crypto');
@@ -142,7 +142,7 @@ async function executeSherlockScan(sherlockPath, username, outputFile, timeout,
142142

143143
return safeSpawnToFile(sherlockPath, args, outputFile, {
144144
timeout: timeout * 1000,
145-
env: { ...process.env, PYTHONUNBUFFERED: '1' }
145+
env: { ...getSafeEnv(), PYTHONUNBUFFERED: '1' }
146146
});
147147
}
148148

docker-compose.yml

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
version: '3.8'
2+
3+
services:
4+
bot:
5+
build: .
6+
restart: unless-stopped
7+
env_file:
8+
- .env
9+
security_opt:
10+
- no-new-privileges:true
11+
cap_drop:
12+
- ALL
13+
read_only: true
14+
tmpfs:
15+
- /app/temp:size=100M,uid=999,gid=999
16+
- /tmp:size=50M
17+
mem_limit: 512m
18+
memswap_limit: 512m
19+
pids_limit: 50

index.js

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,27 @@ const { checkRateLimit } = require('./utils/ratelimit');
2222
const { loadConfig } = require('./utils/config');
2323
const config = loadConfig();
2424

25+
// Clean up orphaned temp files from previous runs
26+
const tempDir = path.join(__dirname, 'temp');
27+
if (fs.existsSync(tempDir)) {
28+
const files = fs.readdirSync(tempDir);
29+
const now = Date.now();
30+
const maxAge = 24 * 60 * 60 * 1000; // 24 hours
31+
for (const file of files) {
32+
try {
33+
const filePath = path.join(tempDir, file);
34+
const stat = fs.statSync(filePath);
35+
if (now - stat.mtimeMs > maxAge) {
36+
if (stat.isDirectory()) {
37+
fs.rmSync(filePath, { recursive: true, force: true });
38+
} else {
39+
fs.unlinkSync(filePath);
40+
}
41+
}
42+
} catch { /* ignore cleanup errors */ }
43+
}
44+
}
45+
2546
// Initialize Discord client with required intents
2647
const client = new Client({
2748
intents: [

0 commit comments

Comments
 (0)