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
23 changes: 23 additions & 0 deletions recipes/tls-parse-cert-string/codemod.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
schema_version: "1.0"
name: "@nodejs/tls-parse-cert-string"
version: "1.0.0"
description: Handle DEP0076 by removing tls.parseCertString() usage and suggesting safer alternatives.
author: Kevin Sailema
license: MIT
workflow: workflow.yaml
category: migration

targets:
languages:
- javascript
- typescript

keywords:
- dep0076
- tls
- parseCertString
- migration

registry:
access: public
visibility: public
24 changes: 24 additions & 0 deletions recipes/tls-parse-cert-string/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
{
"name": "@nodejs/tls-parse-cert-string",
"version": "1.0.0",
"description": "Handle DEP0076 by removing tls.parseCertString() usage and suggesting safer alternatives.",
"type": "module",
"scripts": {
"test": "npx codemod jssg test -l typescript ./src/workflow.ts ./"
},
"repository": {
"type": "git",
"url": "git+https://github.com/nodejs/userland-migrations.git",
"directory": "recipes/tls-parse-cert-string",
"bugs": "https://github.com/nodejs/userland-migrations/issues"
},
"author": "Kevin Sailema",
"license": "MIT",
"homepage": "https://github.com/nodejs/userland-migrations",
"devDependencies": {
"@codemod.com/jssg-types": "^1.5.0"
},
"dependencies": {
"@nodejs/codemod-utils": "*"
}
}
127 changes: 127 additions & 0 deletions recipes/tls-parse-cert-string/src/workflow.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
import { getModuleDependencies } from '@nodejs/codemod-utils/ast-grep/module-dependencies';
import { removeBinding } from '@nodejs/codemod-utils/ast-grep/remove-binding';
import { removeLines } from '@nodejs/codemod-utils/ast-grep/remove-lines';
import { resolveBindingPath } from '@nodejs/codemod-utils/ast-grep/resolve-binding-path';
import type { Edit, Range, SgNode, SgRoot } from '@codemod.com/jssg-types/main';
import type JS from '@codemod.com/jssg-types/langs/javascript';

const COMMENT_ALREADY_PARSED =
'/* DEP0076: cert.subject/cert.issuer are already parsed */';
const COMMENT_MANUAL_PARSE =
'/* DEP0076: use node:crypto X509Certificate for robust parsing */';

function stripOuterParens(text: string): string {
let value = text.trim();
while (value.startsWith('(') && value.endsWith(')')) {
value = value.slice(1, -1).trim();
}
return value;
}

function isAlreadyParsedCertField(argText: string): boolean {
const normalized = stripOuterParens(argText);
return /\.(subject|issuer)$/.test(normalized);
}

function buildReplacement(argText: string): string {
if (isAlreadyParsedCertField(argText)) {
return `${COMMENT_ALREADY_PARSED} ${argText}`;
}

return `${COMMENT_MANUAL_PARSE} Object.fromEntries(String(${argText}).split('/').filter(Boolean).map((pair) => pair.split('=')))`;
}

function trimSingleLeadingBlankLine(sourceCode: string): string {
if (sourceCode.startsWith('\n')) {
return sourceCode.slice(1);
}
return sourceCode;
}

function isInsideNode(node: SgNode<JS>, container: SgNode<JS>): boolean {
for (const ancestor of node.ancestors()) {
if (ancestor.id() === container.id()) return true;
}
return false;
}

function isInsideAnyCall(node: SgNode<JS>, calls: SgNode<JS>[]): boolean {
for (const callNode of calls) {
if (node.id() === callNode.id()) return true;
if (isInsideNode(node, callNode)) return true;
}
return false;
}

function hasNonCallUsage(
rootNode: SgNode<JS>,
statement: SgNode<JS>,
binding: string,
): boolean {
const occurrences = rootNode.findAll({
rule: {
pattern: binding,
},
});

const callOccurrences = rootNode.findAll({
rule: {
pattern: `${binding}($$$ARGS)`,
},
});

for (const occurrence of occurrences) {
if (isInsideNode(occurrence, statement)) continue;
if (isInsideAnyCall(occurrence, callOccurrences)) continue;
return true;
}

return false;
}

export default function transform(root: SgRoot<JS>): string | null {
const rootNode = root.root();
const edits: Edit[] = [];
const linesToRemove: Range[] = [];

const tlsImports = getModuleDependencies(root, 'tls');
if (!tlsImports.length) return null;

const parseBindings = new Set<string>();
for (const stmt of tlsImports) {
const binding = resolveBindingPath(stmt, '$.parseCertString');
if (!binding) continue;
parseBindings.add(binding);
}

if (!parseBindings.size) return null;

for (const binding of parseBindings) {
const callNodes = rootNode.findAll({
rule: {
pattern: `${binding}($ARG)`,
},
});

for (const callNode of callNodes) {
const arg = callNode.getMatch('ARG');
if (!arg) continue;
edits.push(callNode.replace(buildReplacement(arg.text())));
}
}

for (const stmt of tlsImports) {
const binding = resolveBindingPath(stmt, '$.parseCertString');
if (!binding || binding.includes('.')) continue;
if (hasNonCallUsage(rootNode, stmt, binding)) continue;

const result = removeBinding(stmt, binding);
if (result?.edit) edits.push(result.edit);
if (result?.lineToRemove) linesToRemove.push(result.lineToRemove);
}

if (!edits.length && !linesToRemove.length) return null;

const sourceCode = rootNode.commitEdits(edits);
return trimSingleLeadingBlankLine(removeLines(sourceCode, linesToRemove));
}
5 changes: 5 additions & 0 deletions recipes/tls-parse-cert-string/tests/expected/01-cjs-basic.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
const tls = require('node:tls');

const subject = 'C=US/ST=California/L=San Francisco/O=Example/CN=example.com';
const parsed = /* DEP0076: use node:crypto X509Certificate for robust parsing */ Object.fromEntries(String(subject).split('/').filter(Boolean).map((pair) => pair.split('=')));
console.log(parsed);
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
const tls = require('node:tls');

const socket = tls.connect(443, 'example.com', () => {
const cert = socket.getPeerCertificate();
const subject = /* DEP0076: cert.subject/cert.issuer are already parsed */ cert.subject;
console.log(subject);
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
const tls = require('node:tls');

const cert = socket.getPeerCertificate();
const issuer = /* DEP0076: cert.subject/cert.issuer are already parsed */ cert.issuer;
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import tls from 'node:tls';

const parsed = /* DEP0076: use node:crypto X509Certificate for robust parsing */ Object.fromEntries(String('CN=example.com/O=Example').split('/').filter(Boolean).map((pair) => pair.split('=')));
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
const result = /* DEP0076: use node:crypto X509Certificate for robust parsing */ Object.fromEntries(String('C=US/CN=test').split('/').filter(Boolean).map((pair) => pair.split('=')));
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
const { createServer } = require('node:tls');

const parsed = /* DEP0076: use node:crypto X509Certificate for robust parsing */ Object.fromEntries(String('C=US/CN=test').split('/').filter(Boolean).map((pair) => pair.split('=')));
createServer(() => {});
4 changes: 4 additions & 0 deletions recipes/tls-parse-cert-string/tests/expected/07-esm-named.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { connect } from 'node:tls';

const out = /* DEP0076: use node:crypto X509Certificate for robust parsing */ Object.fromEntries(String('CN=example.com/O=Example').split('/').filter(Boolean).map((pair) => pair.split('=')));
connect(443, 'example.com');
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import * as secureTls from 'node:tls';

const parsed = /* DEP0076: use node:crypto X509Certificate for robust parsing */ Object.fromEntries(String(subject).split('/').filter(Boolean).map((pair) => pair.split('=')));
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
const parser = {
parseCertString(value) {
return value;
},
};

const out = parser.parseCertString('CN=example.com');
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
const { parseCertString } = require('node:tls');

const parser = parseCertString;
const parsed = /* DEP0076: use node:crypto X509Certificate for robust parsing */ Object.fromEntries(String('C=US/CN=test').split('/').filter(Boolean).map((pair) => pair.split('=')));
5 changes: 5 additions & 0 deletions recipes/tls-parse-cert-string/tests/input/01-cjs-basic.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
const tls = require('node:tls');

const subject = 'C=US/ST=California/L=San Francisco/O=Example/CN=example.com';
const parsed = tls.parseCertString(subject);
console.log(parsed);
7 changes: 7 additions & 0 deletions recipes/tls-parse-cert-string/tests/input/02-cert-subject.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
const tls = require('node:tls');

const socket = tls.connect(443, 'example.com', () => {
const cert = socket.getPeerCertificate();
const subject = tls.parseCertString(cert.subject);
console.log(subject);
});
4 changes: 4 additions & 0 deletions recipes/tls-parse-cert-string/tests/input/03-cert-issuer.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
const tls = require('node:tls');

const cert = socket.getPeerCertificate();
const issuer = tls.parseCertString(cert.issuer);
3 changes: 3 additions & 0 deletions recipes/tls-parse-cert-string/tests/input/04-esm-default.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import tls from 'node:tls';

const parsed = tls.parseCertString('CN=example.com/O=Example');
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
const { parseCertString } = require('node:tls');

const result = parseCertString('C=US/CN=test');
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
const { parseCertString: pcs, createServer } = require('node:tls');

const parsed = pcs('C=US/CN=test');
createServer(() => {});
4 changes: 4 additions & 0 deletions recipes/tls-parse-cert-string/tests/input/07-esm-named.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { parseCertString, connect } from 'node:tls';

const out = parseCertString('CN=example.com/O=Example');
connect(443, 'example.com');
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import * as secureTls from 'node:tls';

const parsed = secureTls.parseCertString(subject);
7 changes: 7 additions & 0 deletions recipes/tls-parse-cert-string/tests/input/09-not-from-tls.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
const parser = {
parseCertString(value) {
return value;
},
};

const out = parser.parseCertString('CN=example.com');
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
const { parseCertString } = require('node:tls');

const parser = parseCertString;
const parsed = parseCertString('C=US/CN=test');
25 changes: 25 additions & 0 deletions recipes/tls-parse-cert-string/workflow.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# yaml-language-server: $schema=https://raw.githubusercontent.com/codemod-com/codemod/refs/heads/main/schemas/workflow.json

version: "1"

nodes:
- id: apply-transforms
name: Apply AST Transformations
type: automatic
steps:
- name: Remove tls.parseCertString() usage and replace with safer guidance.
js-ast-grep:
js_file: src/workflow.ts
base_path: .
include:
- "**/*.js"
- "**/*.jsx"
- "**/*.mjs"
- "**/*.cjs"
- "**/*.cts"
- "**/*.mts"
- "**/*.ts"
- "**/*.tsx"
exclude:
- "**/node_modules/**"
language: typescript
Loading