Skip to content
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
6 changes: 6 additions & 0 deletions .changeset/spicy-poems-sin.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@nodesecure/contact": major
"@nodesecure/scanner": minor
---

Extract expired email domains
29 changes: 21 additions & 8 deletions workspaces/contact/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,10 +46,10 @@ const extractor = new ContactExtractor({
}
]
});
const contacts = extractor.fromDependencies(
const { illuminated, expired } = extractor.fromDependencies(
dependencies
);
console.log(contacts);
console.log({ illuminated, expired });
```

## API
Expand All @@ -63,28 +63,41 @@ interface Contact {
}
```

> [!NOTE]
> This package authorizes literal RegExp in the name property

### ContactExtractor

The constructor take a list of contacts you want to find/extract.

```ts
interface ContactExtractorOptions {
highlight: Contact[];
highlight: EnforcedContact[];
}

type EnforcedContact = RequireAtLeastOne<
Contact,
"name" | "email"
>;
```

The method **fromDependencies** will return an array of IlluminatedContact objects if any are found in the provided dependencies.
> [!TIP]
> This package authorizes literal RegExp in the name property of `highlight` contacts

The method **fromDependencies** will return an Array of IlluminatedContact objects if any are found in the provided dependencies and the list of expired email domains.

```ts
interface ContactExtractorFromDependenciesResult {
illuminated: IlluminatedContact[];
/**
* List of email domains that are expired
*/
expired: string[];
}

type IlluminatedContact = Contact & {
dependencies: string[];
}
```

### compareContact(contactA: Contact, contactB: Contact, options?: CompareOptions): boolean
### compareContact(contactA: Partial< Contact >, contactB: Partial< Contact >, options?: CompareOptions): boolean

Compare two contacts and return `true` if they are the same person

Expand Down
32 changes: 27 additions & 5 deletions workspaces/contact/src/ContactExtractor.class.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
type EnforcedContact,
type IlluminatedContact
} from "./UnlitContact.class.js";
import { NsResolver } from "./NsResolver.class.js";

export type {
IlluminatedContact,
Expand All @@ -18,6 +19,14 @@ export interface ContactExtractorPackageMetadata {
maintainers: Contact[];
}

export interface ContactExtractorFromDependenciesResult {
illuminated: IlluminatedContact[];
/**
* List of email domains that are expired
*/
expired: string[];
}

export interface ContactExtractorOptions {
highlight: EnforcedContact[];
}
Expand All @@ -28,30 +37,43 @@ export class ContactExtractor {
constructor(
options: ContactExtractorOptions
) {
const { highlight } = options;
const {
highlight
} = options;

this.highlighted = structuredClone(highlight);
}

fromDependencies(
async fromDependencies(
dependencies: Record<string, ContactExtractorPackageMetadata>
): IlluminatedContact[] {
): Promise<ContactExtractorFromDependenciesResult> {
const unlitContacts = this.highlighted
.map((contact) => new UnlitContact(contact));
const resolver = new NsResolver();

for (const [packageName, metadata] of Object.entries(dependencies)) {
const extractedContacts = extractMetadataContacts(metadata);
extractedContacts.forEach((contact) => resolver.registerEmail(contact.email));

for (const unlit of unlitContacts) {
const isMaintainer = extractMetadataContacts(metadata)
const isMaintainer = extractedContacts
.some((contact) => unlit.compareTo(contact));
if (isMaintainer) {
unlit.dependencies.add(packageName);
}
}
}

return unlitContacts.flatMap(
const expired = await resolver.getExpired();

const illuminated = unlitContacts.flatMap(
(unlit) => (unlit.dependencies.size > 0 ? [unlit.illuminate()] : [])
);

return {
expired,
illuminated
};
}
}

Expand Down
53 changes: 53 additions & 0 deletions workspaces/contact/src/NsResolver.class.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
// Import Node.js Dependencies
import { Resolver } from "node:dns/promises";

export class NsResolver {
#dns = new Resolver();
#emails = new Set<string>();

constructor() {
this.#dns.setServers([
"1.1.1.1",
"8.8.8.8"
]);
}

registerEmail(
email: string | undefined | null
) {
// eslint-disable-next-line no-eq-null
if (email == null || email.trim() === "") {
return;
}

this.#emails.add(email);
}

async #resolveNs(
email: string
): Promise<null | string> {
const hostname = email.split("@")[1];

try {
await this.#dns.resolveNs(hostname);

return null;
}
catch {
return email;
}
}

async getExpired() {
const emails = Array.from(this.#emails);

const promises = emails.map(
(email) => this.#resolveNs(email)
);

const expiredEmails = (await Promise.all(promises))
.flatMap((email) => (email === null ? [] : [email]));

return expiredEmails;
}
}
46 changes: 34 additions & 12 deletions workspaces/contact/test/ContactExtractor.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ import {
} from "../src/index.js";

describe("ContactExtractor", () => {
test("Given a contact with no name, it should not throw an Error", () => {
const highlighted: any = {
test("Given a contact with no name, it should not throw an Error", async() => {
const highlighted = {
email: "foobar@gmail.com"
};
const extractor = new ContactExtractor({
Expand All @@ -25,12 +25,13 @@ describe("ContactExtractor", () => {
random: fakePackageMetadata()
};

extractor.fromDependencies(dependencies);
await extractor.fromDependencies(dependencies);
});

describe("fromDependencies", () => {
test(`Given three dependencies where the Highlighted Contact appears two times,
it should successfully scan, extract, and return the contact along with the list of dependencies where it appears.`, () => {
it should successfully scan, extract, and return the contact along with the list of dependencies where it appears.`
, async() => {
const highlighted: Contact = {
name: "john doe"
};
Expand All @@ -44,17 +45,18 @@ describe("ContactExtractor", () => {
random: fakePackageMetadata()
};

const illuminateds = extractor.fromDependencies(dependencies);
const { illuminated } = await extractor.fromDependencies(dependencies);
assert.deepEqual(
illuminateds,
illuminated,
[
{ ...highlighted, dependencies: ["kleur", "mocha"] }
]
);
});

test(`Given a Contact with a RegExp name and three dependencies where the name appears two times,
it should successfully scan, extract, and return the contact along with the list of dependencies where it appears.`, () => {
it should successfully scan, extract, and return the contact along with the list of dependencies where it appears.`
, async() => {
const highlighted: Contact = {
name: "/.*xxaaahelllowwworld.*/i"
};
Expand All @@ -68,9 +70,9 @@ describe("ContactExtractor", () => {
random: fakePackageMetadata()
};

const illuminateds = extractor.fromDependencies(dependencies);
const { illuminated } = await extractor.fromDependencies(dependencies);
assert.deepEqual(
illuminateds,
illuminated,
[
{
...highlighted,
Expand All @@ -81,7 +83,7 @@ describe("ContactExtractor", () => {
});

test(`Given dependencies where the Highlighted Contact doesn't appears,
it must return an empty Array`, () => {
it must return an empty Array`, async() => {
const highlighted: Contact = {
name: "john doe"
};
Expand All @@ -95,8 +97,28 @@ describe("ContactExtractor", () => {
random: fakePackageMetadata()
};

const illuminateds = extractor.fromDependencies(dependencies);
assert.strictEqual(illuminateds.length, 0);
const { illuminated } = await extractor.fromDependencies(dependencies);
assert.strictEqual(illuminated.length, 0);
});

test("Given a Contact with a non-existing email domain, it must be identified as expired", async() => {
const extractor = new ContactExtractor({
highlight: []
});
const expiredEmail = "john.doe+test@somenonexistentdomainongoogle9991254874x54x54.com";

const dependencies: Record<string, ContactExtractorPackageMetadata> = {
kleur: {
author: {
name: "john doe",
email: expiredEmail
},
maintainers: []
}
};

const { expired } = await extractor.fromDependencies(dependencies);
assert.deepEqual(expired, [expiredEmail]);
});
});
});
Expand Down
2 changes: 1 addition & 1 deletion workspaces/scanner/src/utils/warnings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ export async function getDependenciesWarnings(
...kDefaultIlluminatedContacts
]
});
const illuminated = extractor.fromDependencies(
const { illuminated } = await extractor.fromDependencies(
dependencies
);

Expand Down