Skip to content
Open
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
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,5 @@ dist
vs-code-recorder
.vscode-test/
out
test-workspace/vscode-recorder/*
test-workspace/vscode-recorder/*
.DS_Store
10 changes: 10 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,16 @@
{
"command": "crowd-code.consent",
"title": "crowd-code: Manage Data Collection Consent"
},
{
"command": "crowd-code.reloadSecurityFilter",
"title": "crowd-code: Reload Security Filter",
"icon": "$(refresh)"
},
{
"command": "crowd-code.openCrowdCodeIgnore",
"title": "crowd-code: Open .crowdcodeignore File",
"icon": "$(file-text)"
}
],
"viewsContainers": {
Expand Down
12 changes: 12 additions & 0 deletions src/actionsProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,18 @@ export class ActionsProvider implements vscode.TreeDataProvider<ActionItem> {
)
items.push(consentStatus)

// Security Actions
const openCrowdCodeIgnore = new ActionItem(
'Open .crowdcodeignore',
vscode.TreeItemCollapsibleState.None,
{
command: 'crowd-code.openCrowdCodeIgnore',
title: 'Open .crowdcodeignore File',
},
'file-text'
)
items.push(openCrowdCodeIgnore)

return items
}

Expand Down
34 changes: 34 additions & 0 deletions src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import { initializeGitProvider, cleanupGitProvider } from './gitProvider'
import * as fs from 'node:fs'
import * as path from 'node:path'
import { showConsentChangeDialog, ensureConsent, hasConsent } from './consent'
import { reloadSecurityFilter } from './security'

export let statusBarItem: vscode.StatusBarItem
export let extContext: vscode.ExtensionContext
Expand Down Expand Up @@ -191,6 +192,39 @@ export async function activate(context: vscode.ExtensionContext): Promise<void>
})
)

// Register security-related commands
context.subscriptions.push(
vscode.commands.registerCommand('crowd-code.reloadSecurityFilter', async () => {
await reloadSecurityFilter()
vscode.window.showInformationMessage('Security filter reloaded from .crowdcodeignore')
})
)

context.subscriptions.push(
vscode.commands.registerCommand('crowd-code.openCrowdCodeIgnore', async () => {
const workspaceFolder = vscode.workspace.workspaceFolders?.[0]
if (!workspaceFolder) {
vscode.window.showWarningMessage('No workspace folder found')
return
}
const ignoreFilePath = path.join(workspaceFolder.uri.fsPath, '.crowdcodeignore')

if (!fs.existsSync(ignoreFilePath)) {
const create = await vscode.window.showInformationMessage(
'.crowdcodeignore file not found. Create it?',
'Create', 'Cancel'
)
if (create === 'Create') {
// This will trigger creation on next recording start
vscode.window.showInformationMessage('File will be created when recording starts.')
}
return
}

await vscode.window.showTextDocument(vscode.Uri.file(ignoreFilePath))
})
)


context.subscriptions.push(vscode.workspace.onDidChangeConfiguration(onConfigurationChange))

Expand Down
46 changes: 42 additions & 4 deletions src/recording.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {
} from './utilities'
import { type File, ChangeType, type CSVRowBuilder, type Change, type Recording, type ConsentStatus } from './types'
import { extContext, statusBarItem, actionsProvider } from './extension'
import { initializeSecurity, isFileIgnored, containsSensitiveContent, showFilteredContentNotification, getFilteredPlaceholder } from './security'

export const commands = {
openSettings: 'crowd-code.openSettings',
Expand Down Expand Up @@ -91,10 +92,10 @@ export function buildCsvRow({
}

/**
* Checks if the current file being edited is within the configured export path.
* Checks if the current file being edited is within the configured export path or should be ignored for security reasons.
* This is used to determine if the current file should be recorded or not.
*
* @returns {boolean} `true` if the current file is within the export path, `false` otherwise.
* @returns {boolean} `true` if the current file is within the export path or should be ignored, `false` otherwise.
*/
export function isCurrentFileExported(): boolean {
const editor = vscode.window.activeTextEditor
Expand All @@ -103,7 +104,19 @@ export function isCurrentFileExported(): boolean {
if (!editor || !filename || !exportPath) {
return false
}
return filename.startsWith(exportPath)

// Check if file is in export path
if (filename.startsWith(exportPath)) {
return true
}

// Check if file should be ignored for security reasons
if (isFileIgnored(filename)) {
showFilteredContentNotification('File ignored by .crowdcodeignore', path.basename(filename), filename)
return true
}

return false
}

const onChangeSubscription = vscode.workspace.onDidChangeTextDocument(event => {
Expand All @@ -114,16 +127,38 @@ const onChangeSubscription = vscode.workspace.onDidChangeTextDocument(event => {
if (isCurrentFileExported()) {
return
}

const editor = vscode.window.activeTextEditor
if (editor && event.document === editor.document) {
for (const change of event.contentChanges) {
recording.sequence++

let textToRecord = change.text

// Check for sensitive content in the change
const sensitiveCheck = containsSensitiveContent(change.text)
if (sensitiveCheck.isSensitive) {
const matchTypes = sensitiveCheck.matches.map(match => {
if (match.startsWith('sk-') || match.startsWith('pk-')) return 'API key'
if (match.startsWith('eyJ')) return 'JWT token'
if (match.includes('PRIVATE KEY')) return 'Private key'
if (match.includes('://') && match.includes('@')) return 'Connection string'
return 'Sensitive pattern'
}).join(', ')

textToRecord = getFilteredPlaceholder(matchTypes)
showFilteredContentNotification(
'Sensitive content detected',
`${matchTypes} - ${sensitiveCheck.matches.length} pattern(s) found`
)
}

addToFileQueue(
buildCsvRow({
sequence: recording.sequence,
rangeOffset: change.rangeOffset,
rangeLength: change.rangeLength,
text: change.text,
text: textToRecord,
})
)
appendToFile()
Expand All @@ -150,6 +185,9 @@ export async function startRecording(): Promise<void> {
logToOutput('Already recording', 'info')
return
}

// Initialize security system
await initializeSecurity()
const exportPath = getExportPath()
if (!exportPath) {
return
Expand Down
Loading