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

Add ability to customize pattern suggestions #178

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
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
42 changes: 42 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,16 @@
"command": "op-vscode.openLogs",
"title": "Open logs",
"category": "1Password"
},
{
"command": "op-vscode.ignorePattern",
"title": "Ignore pattern",
"category": "1Password"
},
{
"command": "op-vscode.createCustomPattern",
"title": "Create custom pattern",
"category": "1Password"
}
],
"configuration": [
Expand Down Expand Up @@ -129,6 +139,38 @@
"type": "boolean",
"default": false,
"description": "Log debugger data. Reload required."
},
"1password.patterns.disabled": {
"order": 6,
"type": "array",
"items": {
"type": "string"
},
"default": [],
"description": "Patterns that have been disabled from being suggested."
},
"1password.patterns.custom": {
"order": 7,
"type": "array",
"items": {
"type": "object",
"properties": {
"item": {
"type": ["string", "null"],
"description": "The name associated with the pattern."
},
"field": {
"type": ["string", "null"],
"description": "The field that the pattern detects (e.g., password, API token...)."
},
"pattern": {
"type": "string",
"description": "The pattern to detect."
}
}
},
"default": [],
"description": "User-made patterns to be detected and suggested."
}
}
}
Expand Down
4 changes: 4 additions & 0 deletions src/configuration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ export enum ConfigKey {
ItemsUseSecretReferences = "items.useSecretReferences",
EditorSuggestStorage = "editor.suggestStorage",
DebugEnabled = "debug.enabled",
PatternsDisabled = "patterns.disabled",
PatternsCustom = "patterns.custom",
}

interface ConfigItems {
Expand All @@ -16,6 +18,8 @@ interface ConfigItems {
[ConfigKey.ItemsUseSecretReferences]: boolean;
[ConfigKey.EditorSuggestStorage]: boolean;
[ConfigKey.DebugEnabled]: boolean;
[ConfigKey.PatternsDisabled]: string[];
[ConfigKey.PatternsCustom]: object[];
}

class Config {
Expand Down
2 changes: 2 additions & 0 deletions src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ export const COMMANDS = {
INJECT_SECRETS: makeCommand("injectSecrets"),
CREATE_PASSWORD: makeCommand("createPassword"),
OPEN_LOGS: makeCommand("openLogs"),
IGNORE_PATTERN: makeCommand("ignorePattern"),
CREATE_CUSTOM_PATTERN: makeCommand("createCustomPattern"),
};

// This is only internal in that it is not exposed to the
Expand Down
9 changes: 9 additions & 0 deletions src/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { Items } from "./items";
import { logger } from "./logger";
import { Setup } from "./setup";
import { createOpenOPHandler, OpvsUriHandler } from "./url-utils";
import { patterns } from "./secret-detection/patterns";

export class Core {
public cli: CLI;
Expand All @@ -26,6 +27,14 @@ export class Core {
commands.registerCommand(INTERNAL_COMMANDS.AUTHENTICATE, async () =>
this.authenticate(),
),
commands.registerCommand(
COMMANDS.IGNORE_PATTERN,
async (id) => await patterns.disablePattern(id),
),
commands.registerCommand(
COMMANDS.CREATE_CUSTOM_PATTERN,
async () => await patterns.addCustomPattern(),
),
);

this.cli = new CLI();
Expand Down
23 changes: 23 additions & 0 deletions src/language-providers/code-lens.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import GenericParser, * as genericParser from "../secret-detection/parsers/gener
import JsonParser, * as jsonParser from "../secret-detection/parsers/json";
import YamlParser, * as yamlParser from "../secret-detection/parsers/yaml";
import { documentMatcher, provideCodeLenses } from "./code-lens";
import { patterns } from "../secret-detection/patterns";

describe("documentMatcher", () => {
const languageDocument = createDocument([], "properties", "test.js");
Expand Down Expand Up @@ -33,6 +34,28 @@ describe("provideCodeLenses", () => {
expect(codeLenses).toBeUndefined();
});

it("retrieves custom patterns", () => {
const getCustomPatternsSpy = jest
.spyOn(patterns, "getCustomPatterns")
.mockReturnValue([]);
jest.spyOn(config, "get").mockReturnValue(true);
provideCodeLenses(createDocument([]));
expect(getCustomPatternsSpy).toHaveBeenCalled();
});

it("ignores disabled patterns", () => {
jest.spyOn(patterns, "getDisabledPatterns").mockReturnValue(["ccard"]);
jest.spyOn(config, "get").mockReturnValue(true);
const codeLenses = provideCodeLenses(
createDocument([
"Visa 4012888888881881",
"MasterCard 5555555555554444",
"Amex 371449635398431",
]),
);
expect(codeLenses).toHaveLength(0);
});

it("uses the generic parser for an unmatched language", () => {
const genericParserSpy = jest
.spyOn(genericParser, "default")
Expand Down
36 changes: 33 additions & 3 deletions src/language-providers/code-lens.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import DotEnvParser from "../secret-detection/parsers/dotenv";
import GenericParser from "../secret-detection/parsers/generic";
import JsonParser from "../secret-detection/parsers/json";
import YamlParser from "../secret-detection/parsers/yaml";
import { PatternSuggestion } from "../secret-detection/suggestion";
import { patterns } from "../secret-detection/patterns";

export const documentMatcher =
(document: TextDocument) => (ids: string[], exts: string[]) =>
Expand All @@ -31,19 +33,47 @@ export const provideCodeLenses = (document: TextDocument): CodeLens[] => {
parser = new GenericParser(document);
}

return parser
const matches = parser
.getMatches()
.filter(
// Ignore values within secret template variables
({ range, fieldValue, suggestion }) =>
!new RegExp(/\${{(.*?)}}/).test(fieldValue),
)
.map(
.filter((match) => patterns.patternsFilter(match.suggestion));

const customPatternsResult: PatternSuggestion[] =
patterns.getCustomPatterns();
const customPatterns: string[] = Array.isArray(customPatternsResult)
? customPatternsResult.map((suggestion) => suggestion.pattern)
: [];

return [
...matches.map(
({ range, fieldValue, suggestion }) =>
new CodeLens(range, {
title: "$(lock) Save in 1Password",
command: COMMANDS.SAVE_VALUE_TO_ITEM,
arguments: [[{ location: range, fieldValue, suggestion }]],
}),
);
),
...matches
// Don't give the option to ignore custom patterns,
// as they can just be deleted from the settings.json file.
.filter(
(match) =>
match.suggestion !== undefined &&
!customPatterns.includes(
(match.suggestion as PatternSuggestion).pattern,
),
)
.map(
({ range, fieldValue, suggestion }) =>
new CodeLens(range, {
title: "Ignore pattern",
command: COMMANDS.IGNORE_PATTERN,
arguments: [(suggestion as PatternSuggestion).id],
}),
),
];
};
10 changes: 9 additions & 1 deletion src/secret-detection/parsers/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import {
validValueIsolation,
} from ".";
import { sample } from "../../../test/utils";
import { getPatternSuggestion } from "../patterns";
import { patterns, getPatternSuggestion } from "../patterns";
import { BRANDS } from "../suggestion";

describe("findBrand", () => {
Expand Down Expand Up @@ -68,6 +68,14 @@ describe("matchFromRegexp", () => {
suggestion,
});
});

it("retrieves custom patterns", () => {
const customPatternsSpy = jest
.spyOn(patterns, "getCustomPatterns")
.mockReturnValue([]);
matchFromRegexp("test");
expect(customPatternsSpy).toHaveBeenCalled();
});
});

describe("suggestionFromKey", () => {
Expand Down
20 changes: 16 additions & 4 deletions src/secret-detection/parsers/index.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
import { FieldAssignmentType } from "@1password/op-js";
import { Range, TextDocument } from "vscode";
import { combineRegexp } from "../../utils";
import { getPatternSuggestion, VALUE_PATTERNS } from "../patterns";
import { BRANDS, SECRET_KEY_HINT, Suggestion } from "../suggestion";
import { patterns, getPatternSuggestion, VALUE_PATTERNS } from "../patterns";
import {
BRANDS,
PatternSuggestion,
SECRET_KEY_HINT,
Suggestion,
} from "../suggestion";

export interface ParserMatch {
range: Range;
Expand Down Expand Up @@ -35,6 +40,7 @@ export const patternSuggestions = [
...VALUE_PATTERNS,
getPatternSuggestion("ccard"),
];

const patternsRegex = combineRegexp(
...patternSuggestions.map((detection) => new RegExp(detection.pattern)),
);
Expand Down Expand Up @@ -66,7 +72,12 @@ export const matchFromRegexp = (
input: string,
partial = false,
): MatchDetail | undefined => {
const patternMatch = patternsRegex.exec(input);
const customPatterns: PatternSuggestion[] = patterns.getCustomPatterns();
const allPatternsRegex = combineRegexp(
patternsRegex,
...customPatterns.map((suggestion) => new RegExp(suggestion.pattern)),
);
const patternMatch = allPatternsRegex.exec(input);
if (!patternMatch) {
return;
}
Expand All @@ -84,7 +95,8 @@ export const matchFromRegexp = (

// We know that the value matches one of the patterns,
// now let's find out which one
for (const patternSuggestion of patternSuggestions) {
const allPatternSuggestions = [...patternSuggestions, ...customPatterns];
for (const patternSuggestion of allPatternSuggestions) {
if (new RegExp(patternSuggestion.pattern).test(value)) {
suggestion = patternSuggestion;

Expand Down
28 changes: 27 additions & 1 deletion src/secret-detection/patterns.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
import testData from "./pattern-test-data.json";
import { FIELD_TYPE_PATTERNS, getPatternSuggestion } from "./patterns";
import {
FIELD_TYPE_PATTERNS,
getPatternSuggestion,
patterns,
} from "./patterns";
import { ConfigKey, config } from "../configuration";

describe("getPatternSuggestion", () => {
it("should return a pattern suggestion", () => {
Expand Down Expand Up @@ -69,3 +74,24 @@ describe("VALUE_PATTERNS", () => {
expect(value).toMatchRegExp(new RegExp(patternSuggestion.pattern));
});
});

describe("patterns", () => {
describe("getDisabledPatterns", () => {
it("should return an empty array if no disabled patterns are set", () => {
expect(patterns.getDisabledPatterns()).toEqual([]);
});
});
describe("getCustomPatterns", () => {
it("should return an empty array if no custom patterns are set", () => {
expect(patterns.getCustomPatterns()).toEqual([]);
});
});
describe("patternsFilter", () => {
it("should filter out disabled patterns", () => {
jest.spyOn(patterns, "getDisabledPatterns").mockReturnValue(["ccard"]);
expect(
patterns.patternsFilter(getPatternSuggestion("ccard")),
).toBeFalsy();
});
});
});
67 changes: 66 additions & 1 deletion src/secret-detection/patterns.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,68 @@
import { PatternSuggestion } from "./suggestion";
import { PatternSuggestion, Suggestion } from "./suggestion";
import { window } from "vscode";
import { config, ConfigKey } from "../configuration";

export class Patterns {
public getCustomPatterns(): PatternSuggestion[] {
return config.get<PatternSuggestion[]>(ConfigKey.PatternsCustom) || [];
}

public getDisabledPatterns(): string[] {
return config.get<string[]>(ConfigKey.PatternsDisabled) || [];
}

public async disablePattern(id: string): Promise<void> {
const newDisabledPatterns = [...this.getDisabledPatterns(), id];
await config.set(ConfigKey.PatternsDisabled, newDisabledPatterns).then(
() => window.showInformationMessage(`Pattern ${id} disabled.`),
() => window.showErrorMessage(`Could not disable pattern.`),
);
}

public async addCustomPattern(): Promise<void> {
const regex = await window.showInputBox({
title: "Enter a regex pattern.",
ignoreFocusOut: true,
});

if (!regex || regex.length === 0) {
return;
}

const item = await window.showInputBox({
title: "Enter an item name (optional).",
ignoreFocusOut: true,
});

const field = await window.showInputBox({
title: "Enter a field name (optional).",
ignoreFocusOut: true,
});

const newPattern = {
item,
field,
pattern: regex,
};

const customPatterns = [...this.getCustomPatterns(), newPattern];
await config.set(ConfigKey.PatternsCustom, customPatterns).then(
() => window.showInformationMessage(`Custom pattern added`),
() => window.showErrorMessage(`Could not add custom pattern.`),
);
}

public patternsFilter(suggestion: Suggestion) {
// If the suggestion is not a PatternSuggestion or the suggestion is not disabled
return (
suggestion === undefined ||
(suggestion as PatternSuggestion).id === undefined ||
!this.getDisabledPatterns().includes((suggestion as PatternSuggestion).id)
);
}
}

export const patterns = new Patterns();

export const getPatternSuggestion = (id: string): PatternSuggestion =>
[...FIELD_TYPE_PATTERNS, ...VALUE_PATTERNS].find(
Expand Down Expand Up @@ -248,4 +312,5 @@ export const VALUE_PATTERNS: PatternSuggestion[] = [
pattern: "https://chat.twilio.com/v2/Services/[A-Z0-9]{32}",
},
];

/* eslint-enable sonarjs/no-duplicate-string */
Loading