Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(cli): Secret scan #438

Merged
merged 20 commits into from
Sep 17, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions apps/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,11 @@
"eccrypto": "^1.1.6",
"figlet": "^1.7.0",
"fs": "0.0.1-security",
"glob": "^11.0.0",
"nodemon": "^3.1.4",
"socket.io-client": "^4.7.5"
"secret-scan": "workspace:*",
"socket.io-client": "^4.7.5",
"typescript": "^5.5.2"
},
"devDependencies": {
"@swc/cli": "^0.4.0",
Expand All @@ -32,6 +35,7 @@
"@types/figlet": "^1.5.8",
"@types/eccrypto": "^1.1.6",
"@types/node": "^20.14.10",
"eslint-config-standard-with-typescript": "^43.0.1"
"eslint-config-standard-with-typescript": "^43.0.1",
"tsup": "^8.1.2"
}
}
179 changes: 179 additions & 0 deletions apps/cli/src/commands/scan.command.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
import BaseCommand from '@/commands/base.command'
import type {
CommandActionData,
CommandOption
} from '@/types/command/command.types'
import { execSync } from 'child_process'
import { readFileSync, statSync } from 'fs'
import { globSync } from 'glob'
import path from 'path'
import secretDetector from 'secret-scan'
// eslint-disable-next-line @typescript-eslint/no-var-requires
const colors = require('colors/safe')

export default class ScanCommand extends BaseCommand {
getOptions(): CommandOption[] {
return [
{
short: '-f',
long: '--file <string>',
description: 'Scan a specific file'
},
{
short: '-c',
long: '--current-changes',
description:
'Scan only the current changed files that are not committed'
}
]
}

getName(): string {
return 'scan'
}

getDescription(): string {
return 'Scan the project to detect any hardcoded secrets'
}

action({ options }: CommandActionData): Promise<void> | void {
const { currentChanges, file } = options

console.log('\n=============================')
console.log(colors.cyan.bold('🔍 Secret Scan Started'))
console.log('=============================')

if (file) {
console.log(`Scanning file: ${file}`)
this.scanFiles([file as string])
return
}
if (currentChanges) {
console.log('Scanning only the current changes')
const files = this.getChangedFiles()
this.scanFiles(files)
} else {
console.log('\n\n📂 Scanning all files...\n')
const files = this.getAllFiles()
this.scanFiles(files)
}
}

private scanFiles(allfiles: string[]) {
const foundSecrets = []
let skipNextLine = false
for (const file of allfiles) {
const stats = statSync(file)
if (stats.isFile()) {
const content = readFileSync(file, 'utf8').split(/\r?\n/)

// Skip the file if ignore comment is found in the first line
if (content[0].includes('keyshade-ignore-all')) {
continue
}

content.forEach((line, index) => {
// Skip the next line if ignore comment is found in the previous line
if (skipNextLine) {
skipNextLine = false
return
}

if (line.includes('keyshade-ignore')) {
skipNextLine = true
return
}
const { found, regex } = secretDetector.detect(line)
if (found) {
const matched = line.match(regex)
const highlightedLine = line
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
.replace(regex, colors.red.underline(matched[0]) as string)
.trim()
foundSecrets.push({
file,
line: index + 1,
content: highlightedLine
})
}
})
}
}
if (foundSecrets.length > 0) {
console.log(
colors.red(`\n 🚨 Found ${foundSecrets.length} hardcoded secrets:\n`)
)
foundSecrets.forEach((secret) => {
console.log(
`${colors.underline(`${secret.file}:${secret.line}`)}: ${secret.content}`
)
console.log(
colors.yellow(
'Suggestion: Replace with environment variables or secure storage solutions.\n'
)
)
})
console.log('=============================')
console.log('Summary:')
console.log('=============================')
console.log(`🚨 Total Secrets Found: ${foundSecrets.length}\n`)

process.exit(1)
} else {
console.log('=============================')
console.log('Summary:')
console.log('=============================')
console.log('✅ Total Secrets Found: 0\n')
console.log(colors.green('No hardcoded secrets found.'))
}
}

private getAllFiles(): string[] {
const currentWorkDir = process.cwd()
let gitIgnorePatterns: string[] = []
try {
const gitIgnorePath = path.resolve(currentWorkDir, '.gitignore')

const gitIgnoreContent = readFileSync(gitIgnorePath, 'utf8')

gitIgnorePatterns = gitIgnoreContent
.split('\n')
.filter((line) => line.trim() !== '' && !line.startsWith('#'))
} catch (error) {
console.log("Repository doesn't have .gitignore file")
}

return globSync(currentWorkDir + '/**/**', {
dot: true,
ignore: {
ignored: (p) => {
return gitIgnorePatterns.some((pattern) => {
return p.isNamed(pattern)
})
},
childrenIgnored: (p) => {
return gitIgnorePatterns.some((pattern) => {
return p.isNamed(pattern)
})
}
}
})
}

private getChangedFiles(): string[] {
const output = execSync('git status -s').toString()
const files = output
.split('\n')
.filter((line) => {
if (typeof line === 'undefined') {
return false
}
return line
})
.map((line) => {
line = line.trim().split(' ')[1]
return path.resolve(process.cwd(), line)
})
return files
}
}
4 changes: 3 additions & 1 deletion apps/cli/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import InitCommand from './commands/init.command'
import RunCommand from './commands/run.command'
import ProfileCommand from './commands/profile.command'
import EnvironmentCommand from './commands/environment.command'
import ScanCommand from '@/commands/scan.command'

const program = new Command()

Expand All @@ -15,7 +16,8 @@ const COMMANDS: BaseCommand[] = [
new RunCommand(),
new InitCommand(),
new ProfileCommand(),
new EnvironmentCommand()
new EnvironmentCommand(),
new ScanCommand()
]

COMMANDS.forEach((command) => {
Expand Down
10 changes: 8 additions & 2 deletions apps/cli/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,14 @@
"verbose": false
},
"ts-node": {
"require": ["tsconfig-paths/register"]
"require": ["tsconfig-paths/register"],
"skipLibCheck": true,
"strictNullChecks": false,
"noImplicitAny": false,
"strictBindCallApply": false,
"forceConsistentCasingInFileNames": true,
"noFallthroughCasesInSwitch": true
},
"include": ["src/**/*.ts"],
"include": ["src/**/*.ts", "tsup.config.ts"],
"exclude": ["node_modules"]
}
15 changes: 15 additions & 0 deletions apps/cli/tsup.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import type { Options } from 'tsup'
import { defineConfig } from 'tsup'

const env = process.env.NODE_ENV

export default defineConfig((options: Options) => ({
plugins: [],
treeshake: true,
splitting: true,
entryPoints: ['src/index.ts'],
dts: true,
minify: env === 'production',
clean: true,
...options
}))
1 change: 1 addition & 0 deletions apps/platform/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ COPY --from=installer --chown=nextjs:nodejs /app/apps/platform/public ./apps/pl
ENV PORT=3025
ENV NEXT_SHARP_PATH=/app/apps/platform/.next/sharp
EXPOSE 3025
# keyshade-ignore
ENV HOSTNAME "0.0.0.0"


Expand Down
1 change: 1 addition & 0 deletions apps/web/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ COPY --from=installer --chown=nextjs:nodejs /app/apps/web/public ./apps/web/pub
ENV PORT=3000
ENV NEXT_SHARP_PATH=/app/apps/web/.next/sharp
EXPOSE 3000
# keyshade-ignore
ENV HOSTNAME "0.0.0.0"


Expand Down
31 changes: 31 additions & 0 deletions packages/secret-scan/.eslintrc.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
module.exports = {
parser: '@typescript-eslint/parser',
parserOptions: {
project: 'tsconfig.json',
tsconfigRootDir: __dirname,
sourceType: 'module'
},
plugins: ['@typescript-eslint/eslint-plugin'],
extends: [
'plugin:@typescript-eslint/recommended',
'plugin:prettier/recommended',
'standard-with-typescript'
],
root: true,
env: {
node: true,
jest: true
},
ignorePatterns: ['.eslintrc.js'],
rules: {
'@typescript-eslint/interface-name-prefix': 'off',
'@typescript-eslint/explicit-function-return-type': 'off',
'@typescript-eslint/explicit-module-boundary-types': 'off',
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/no-unused-vars': ['warn'],
'@typescript-eslint/space-before-function-paren': 'off',
'@typescript-eslint/strict-boolean-expressions': 'off',
'@typescript-eslint/prefer-nullish-coalescing': 'off',
'space-before-function-paren': 'off',
}
}
5 changes: 5 additions & 0 deletions packages/secret-scan/.mocharc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"require": "tsx",
"spec": "src/test/**/*.ts",
"extension": ["ts"]
}
28 changes: 28 additions & 0 deletions packages/secret-scan/.swcrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
{
"$schema": "https://json.schemastore.org/swcrc",
"jsc": {
"parser": {
"syntax": "typescript",
"tsx": false,
"dynamicImport": true,
"decorators": false
},
"transform": null,
"target": "es2022",
"loose": false,
"externalHelpers": false,
"keepClassNames": false,
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
}
},
"module": {
"type": "commonjs",
"strict": false,
"strictMode": false,
"noInterop": false,
"lazy": false
},
"minify": true
}
30 changes: 30 additions & 0 deletions packages/secret-scan/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
{
"name": "secret-scan",
"version": "1.0.0",
"description": "Do static analysis of a string to find secrets",
"main": "dist/index.js",
"scripts": {
"genKey": "tsx src/generateKey.ts",
"build": "tsup",
"test": "mocha"
},
"publishConfig": {
"access": "public"
},
"exports": {
".": "./dist/index.js"
},
"dependencies": {
"mocha": "^10.6.0",
"randexp": "^0.5.3",
"tsx": "^4.16.2"
},
"devDependencies": {
"@types/mocha": "^10.0.7",
"@types/node": "^20.14.10",
"eslint-config-standard-with-typescript": "^43.0.1",
"jest": "^29.5.0",
"tsup": "^8.1.2",
"typescript": "^5.5.3"
}
}
Loading
Loading