Is @hasna/domains safe?
- Node.js shell/command execution
- npm install-time lifecycle hook
- Node.js filesystem read
@hasna/domains is an AI npm_package analyzed by SkillTotal's deterministic static scanner. The scan found no malicious indicators, though 6 risky constructs are reported for review. It can: filesystem read, install time execution, mcp tools detected, network egress, scoped identity and shell execution — capabilities are what the code can do, not a verdict on intent. Risk score 0/100 (low).
@hasna/domains 0.3.1
Automated static-analysis result. It can contain false positives and false negatives, and is not a claim about the intent of @hasna/domains's authors. Report a false positive.
Behavioral traits
How this component maps to the CSA agentic threat model. Descriptive — it never affects the risk score.
Findings (6)
package.json runs scripts automatically when the package is installed.
"postinstall": "node postinstall.js",
Why it matters: Install scripts are a favorite supply-chain foothold — they execute on every machine that installs the package.
Fix: Inspect the hook command. Install-time scripts are a common supply chain execution vector; ensure they do nothing beyond a documented build step.
The component can run operating-system commands or spawn processes.
var childProcess = __require("child_process");import { spawnSync } from "child_process";const result = spawnSync(KEYCHAIN_SECURITY_BIN, [...argv], {import { execFileSync } from "child_process";import { exec } from "child_process";import { execFile } from "child_process";import { execFile as execFile2 } from "child_process";import { spawn } from "child_process";const child = spawn(channel.command.command, channel.command.args ?? [], {import { execSync } from "child_process";execSync("whois --version 2>/dev/null || whois example.com 2>/dev/null", { timeout: 3000 });import { execSync as execSync2 } from "child_process";import { exec } from "child_process";import { spawnSync } from "child_process";const result = spawnSync(KEYCHAIN_SECURITY_BIN, [...argv], {import { execFileSync } from "child_process";import { exec } from "child_process";import { spawnSync } from "child_process";const result = spawnSync(KEYCHAIN_SECURITY_BIN, [...argv], {import { execFileSync } from "child_process";import { spawnSync } from "child_process";const result = spawnSync(KEYCHAIN_SECURITY_BIN, [...argv], {import { exec } from "child_process";Why it matters: Powerful and often legitimate — confirm the commands aren't built from untrusted input.
Fix: Confirm the command and its arguments are fully controlled and not derived from untrusted input; prefer execFile with an argument array.
The component reads files from disk.
request.headers.Authorization = validateToken((await fs.readFile(tokenFile)).toString());
request.headers.Authorization = validateToken((await fs.readFile(tokenFile)).toString());
request.headers.Authorization = validateToken((await fs.readFile(tokenFile)).toString());
request.headers.Authorization = validateToken((await fs.readFile(tokenFile)).toString());
Why it matters: Usually legitimate, but worth confirming it can't be steered into reading sensitive files.
Fix: Confirm which files are read and that paths cannot be influenced by untrusted input to reach sensitive locations.
The component makes outbound network requests.
const fetchImpl = options.fetchImpl ?? ((input, init) => fetch(input, init));
return this.http.get(`/offers/${enc(id)}`).catch(() => null);const res = await this.http.get(`/domains/${enc(domainId)}/offers`);return this.http.get(`/emails/${enc(id)}`).catch(() => null);const res = await this.http.get(`/domains/${enc(domainId)}/emails`);const res = await this.http.get(`/domains/${enc(domainId)}/dns`, { query: q({ type }) });const res = await this.http.get(`/domains/${enc(domainId)}/alerts`);const res = await this.http.get(`/domains/${enc(domainId)}/owners`);const res = await this.http.get(`/owners-portfolio`);
const res = await this.http.get(`/domains/${enc(domainId)}/history`, {const res = await this.http.get(`/history`, {const res = await this.http.get(`/history-changes`);
return this.http.get(`/domains/${enc(domainId)}/reputation`).catch(() => null);const response = await fetch(url, {const response = await fetch(url, {const response = await fetch(`${CF_BASE}${path}`, {const resp = await fetch(url);
const response = await fetch(`https://${input.hostname}/`, {const response = await fetch(url.toString(), {const raw = path ? readFileSync9(path, "utf8") : await (await fetch(IANA_RDAP_BOOTSTRAP, { headers: { Accept: "application/json" } })).text();const res = await fetch(`${base}domain/${name}`, {const fetchImpl = options.fetchImpl ?? ((input, init) => fetch(input, init));
return this.http.get(`/offers/${enc(id)}`).catch(() => null);const res = await this.http.get(`/domains/${enc(domainId)}/offers`);return this.http.get(`/emails/${enc(id)}`).catch(() => null);Why it matters: Usually legitimate, but confirm the destinations are expected and no sensitive data leaves.
Fix: Confirm the destination hosts are expected and that no sensitive data is sent off-host.
A short-lived, scoped, assumed identity was detected — an STS AssumeRole / session token, a cloud managed or workload identity, an impersonated service account, a projected Kubernetes service-account token, or a dynamic-secret broker. Tools authenticate with a narrowly-scoped credential that expires, rather than a long-lived embedded service credential. (25 occurrence(s) shown as evidence).
case "AssumeRoleWithWebIdentity": {var _AR = "AssumeRole";
var _ARWWI = "AssumeRoleWithWebIdentity";
var AssumeRole$ = [
var AssumeRoleWithWebIdentity$ = [
class AssumeRoleCommand extends command2(_ep02, _mw02, "AssumeRole", AssumeRole$) {class AssumeRoleWithWebIdentityCommand extends command2(_ep02, _mw02, "AssumeRoleWithWebIdentity", AssumeRoleWithWebIdentity$) {exports.AssumeRole$ = AssumeRole$;
exports.AssumeRoleWithWebIdentity$ = AssumeRoleWithWebIdentity$;
case "AssumeRoleWithWebIdentity": {var _AR = "AssumeRole";
var _ARWWI = "AssumeRoleWithWebIdentity";
var AssumeRole$ = [
var AssumeRoleWithWebIdentity$ = [
class AssumeRoleCommand extends command2(_ep02, _mw02, "AssumeRole", AssumeRole$) {class AssumeRoleWithWebIdentityCommand extends command2(_ep02, _mw02, "AssumeRoleWithWebIdentity", AssumeRoleWithWebIdentity$) {exports.AssumeRole$ = AssumeRole$;
exports.AssumeRoleWithWebIdentity$ = AssumeRoleWithWebIdentity$;
case "AssumeRoleWithWebIdentity": {var _AR = "AssumeRole";
var _ARWWI = "AssumeRoleWithWebIdentity";
var AssumeRole$ = [
var AssumeRoleWithWebIdentity$ = [
class AssumeRoleCommand extends command2(_ep02, _mw02, "AssumeRole", AssumeRole$) {class AssumeRoleWithWebIdentityCommand extends command2(_ep02, _mw02, "AssumeRoleWithWebIdentity", AssumeRoleWithWebIdentity$) {Fix: A scoped, short-lived identity is the smallest-blast-radius execution context. Confirm the assumed role / requested scope grants only the permissions the tool needs, and that the token lifetime is minimal.
An MCP tool surface (manifest or tool definitions) was found.
const server = new McpServer({server.registerTool("create_domain", {server.registerTool("get_domain", {server.registerTool("list_domains", {server.registerTool("update_domain", {server.registerTool("mark_domain_premium", {server.registerTool("add_domain_offer", {server.registerTool("list_domain_offers", {server.registerTool("update_domain_status", {server.registerTool("record_domain_purchase", {server.registerTool("link_domain_email", {server.registerTool("get_domain_emails", {server.registerTool("delete_domain", {server.registerTool("search_domains", {server.registerTool("count_domains", {server.registerTool("list_expiring_domains", {server.registerTool("list_ssl_expiring", {server.registerTool("get_domains_by_registrar", {server.registerTool("get_domain_stats", {server.registerTool("create_dns_record", {server.registerTool("list_dns_records", {server.registerTool("update_dns_record", {server.registerTool("delete_dns_record", {server.registerTool("create_alert", {server.registerTool("list_alerts", {Why it matters: Just context — review which tools it offers and their permissions.
Fix: Review the declared MCP tools and their permissions.
Check your own component
Run the same evidence-backed scan on any MCP server, agent skill, or package.
Scan your own componentHow we determine this: deterministic static analysis (regex + AST), evidence-anchored, no code execution. Methodology →