diff --git a/libs/importer/spec/netwrix-passwordsecure-csv-importer.spec.ts b/libs/importer/spec/netwrix-passwordsecure-csv-importer.spec.ts new file mode 100644 index 00000000000..ab893dbc56c --- /dev/null +++ b/libs/importer/spec/netwrix-passwordsecure-csv-importer.spec.ts @@ -0,0 +1,91 @@ +import { Utils } from "@bitwarden/common/platform/misc/utils"; + +import { NetwrixPasswordSecureCsvImporter } from "../src/importers"; + +import { credentialsData } from "./test-data/netwrix-csv/login-export.csv"; + +describe("Netwrix Password Secure CSV Importer", () => { + let importer: NetwrixPasswordSecureCsvImporter; + beforeEach(() => { + importer = new NetwrixPasswordSecureCsvImporter(); + }); + + it("passing invalid data returns false", async () => { + const result = await importer.parse(""); + expect(result != null).toBe(true); + expect(result.success).toBe(false); + }); + + it("should parse login records", async () => { + const result = await importer.parse(credentialsData); + expect(result != null).toBe(true); + + let cipher = result.ciphers.shift(); + expect(cipher.name).toEqual("Test Entry 1"); + expect(cipher.login.username).toEqual("someUser"); + expect(cipher.login.password).toEqual("somePassword"); + expect(cipher.login.totp).toEqual("someTOTPSeed"); + expect(cipher.login.uris.length).toEqual(1); + let uriView = cipher.login.uris.shift(); + expect(uriView.uri).toEqual("https://www.example.com"); + expect(cipher.notes).toEqual("some note for example.com"); + + cipher = result.ciphers.shift(); + expect(cipher.name).toEqual("Test Entry 2"); + expect(cipher.login.username).toEqual("jdoe"); + expect(cipher.login.password).toEqual("})9+Kg2fz_O#W1§H1-0Zio"); + expect(cipher.login.totp).toEqual("anotherTOTP"); + expect(cipher.login.uris.length).toEqual(1); + uriView = cipher.login.uris.shift(); + expect(uriView.uri).toEqual("http://www.123.com"); + expect(cipher.notes).toEqual("Description123"); + + cipher = result.ciphers.shift(); + expect(cipher.name).toEqual("Test Entry 3"); + expect(cipher.login.username).toEqual("username"); + expect(cipher.login.password).toEqual("password"); + expect(cipher.login.totp).toBeNull(); + expect(cipher.login.uris.length).toEqual(1); + uriView = cipher.login.uris.shift(); + expect(uriView.uri).toEqual("http://www.internetsite.com"); + expect(cipher.notes).toEqual("Information"); + }); + + it("should add any unmapped fields as custom fields", async () => { + const result = await importer.parse(credentialsData); + expect(result != null).toBe(true); + + const cipher = result.ciphers.shift(); + expect(cipher.fields.length).toBe(1); + const field = cipher.fields.shift(); + expect(field.name).toEqual("DataTags"); + expect(field.value).toEqual("tag1, tag2, tag3"); + }); + + it("should parse an item and create a folder", async () => { + const result = await importer.parse(credentialsData); + + expect(result).not.toBeNull(); + expect(result.success).toBe(true); + expect(result.folders.length).toBe(2); + expect(result.folders[0].name).toBe("folderOrCollection1"); + expect(result.folders[1].name).toBe("folderOrCollection2"); + expect(result.folderRelationships[0]).toEqual([0, 0]); + expect(result.folderRelationships[1]).toEqual([1, 1]); + expect(result.folderRelationships[2]).toEqual([2, 0]); + }); + + it("should parse an item and create a collection when importing into an organization", async () => { + importer.organizationId = Utils.newGuid(); + const result = await importer.parse(credentialsData); + + expect(result).not.toBeNull(); + expect(result.success).toBe(true); + expect(result.collections.length).toBe(2); + expect(result.collections[0].name).toBe("folderOrCollection1"); + expect(result.collections[1].name).toBe("folderOrCollection2"); + expect(result.collectionRelationships[0]).toEqual([0, 0]); + expect(result.collectionRelationships[1]).toEqual([1, 1]); + expect(result.collectionRelationships[2]).toEqual([2, 0]); + }); +}); diff --git a/libs/importer/spec/test-data/netwrix-csv/login-export.csv.ts b/libs/importer/spec/test-data/netwrix-csv/login-export.csv.ts new file mode 100644 index 00000000000..715dd8e0074 --- /dev/null +++ b/libs/importer/spec/test-data/netwrix-csv/login-export.csv.ts @@ -0,0 +1,4 @@ +export const credentialsData = `"Organisationseinheit";"DataTags";"Beschreibung";"Benutzername";"Passwort";"Internetseite";"Informationen";"One-Time Passwort" +"folderOrCollection1";"tag1, tag2, tag3";"Test Entry 1";"someUser";"somePassword";"https://www.example.com";"some note for example.com";"someTOTPSeed" +"folderOrCollection2";"tag2";"Test Entry 2";"jdoe";"})9+Kg2fz_O#W1§H1-0Zio";"www.123.com";"Description123";"anotherTOTP" +"folderOrCollection1";"someTag";"Test Entry 3";"username";"password";"www.internetsite.com";"Information";""`; diff --git a/libs/importer/src/components/import.component.html b/libs/importer/src/components/import.component.html index 91ad7dbfc0a..5b67fc47a78 100644 --- a/libs/importer/src/components/import.component.html +++ b/libs/importer/src/components/import.component.html @@ -380,6 +380,10 @@ In the ProtonPass browser extension, go to Settings > Export. Export without PGP encryption and save the zip file. + + Open the FullClient, go to the Main Menu and select Export. Start the export passwords + wizard and follow the instructions to export a CSV file. + { + const result = new ImportResult(); + const results = this.parseCsv(data, true); + if (results == null) { + result.success = false; + return Promise.resolve(result); + } + + results.forEach((row: LoginRecord) => { + this.processFolder(result, row.Organisationseinheit); + const cipher = this.initLoginCipher(); + + const notes = this.getValueOrDefault(row.Informationen); + if (notes) { + cipher.notes = `${notes}\n`; + } + + cipher.name = this.getValueOrDefault(row.Beschreibung, "--"); + cipher.login.username = this.getValueOrDefault(row.Benutzername); + cipher.login.password = this.getValueOrDefault(row.Passwort); + cipher.login.uris = this.makeUriArray(row.Internetseite); + + cipher.login.totp = this.getValueOrDefault(row["One-Time Passwort"]); + + this.importUnmappedFields(cipher, row, _mappedColumns); + + this.cleanupCipher(cipher); + result.ciphers.push(cipher); + }); + + if (this.organization) { + this.moveFoldersToCollections(result); + } + + result.success = true; + return Promise.resolve(result); + } + + private importUnmappedFields(cipher: CipherView, row: any, mappedValues: Set) { + const unmappedFields = Object.keys(row).filter((x) => !mappedValues.has(x)); + unmappedFields.forEach((key) => { + const item = row as any; + this.processKvp(cipher, key, item[key]); + }); + } +} diff --git a/libs/importer/src/importers/netwrix/netwrix-passwordsecure-csv-types.ts b/libs/importer/src/importers/netwrix/netwrix-passwordsecure-csv-types.ts new file mode 100644 index 00000000000..63a4255805e --- /dev/null +++ b/libs/importer/src/importers/netwrix/netwrix-passwordsecure-csv-types.ts @@ -0,0 +1,18 @@ +export class LoginRecord { + /** Organization unit / folder / collection */ + Organisationseinheit: string; + /** Tags? */ + DataTags: string; + /** Description/title */ + Beschreibung: string; + /** Username */ + Benutzername: string; + /** Password */ + Passwort: string; + /** URL */ + Internetseite: string; + /** Notes/additional information */ + Informationen: string; + /** TOTP */ + "One-Time Passwort": string; +} diff --git a/libs/importer/src/models/import-options.ts b/libs/importer/src/models/import-options.ts index 64546cc57b3..f656c728ffd 100644 --- a/libs/importer/src/models/import-options.ts +++ b/libs/importer/src/models/import-options.ts @@ -70,6 +70,7 @@ export const regularImportOptions = [ { id: "nordpasscsv", name: "Nordpass (csv)" }, { id: "psonojson", name: "Psono (json)" }, { id: "passkyjson", name: "Passky (json)" }, + { id: "netwrixpasswordsecure", name: "Netwrix Password Secure (csv)" }, ] as const; export type ImportType = diff --git a/libs/importer/src/services/import.service.ts b/libs/importer/src/services/import.service.ts index 17695c29d57..6bfc5d5ce99 100644 --- a/libs/importer/src/services/import.service.ts +++ b/libs/importer/src/services/import.service.ts @@ -54,6 +54,7 @@ import { MSecureCsvImporter, MeldiumCsvImporter, MykiCsvImporter, + NetwrixPasswordSecureCsvImporter, NordPassCsvImporter, OnePassword1PifImporter, OnePassword1PuxImporter, @@ -335,6 +336,8 @@ export class ImportService implements ImportServiceAbstraction { return new PasskyJsonImporter(); case "protonpass": return new ProtonPassJsonImporter(this.i18nService); + case "netwrixpasswordsecure": + return new NetwrixPasswordSecureCsvImporter(); default: return null; }