SkillTotal

Is @hasna/domains safe?

No malicious indicators - review capabilities before installing
Notable — review in context (capabilities are not malware):
  • 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

npm_package · npm:@hasna/domains
LOW
0
/ 100 risk score
Snapshot · scanned Sep 25, 2026 · @hasna/domains@0.3.1 · engine 0.53.0 / ruleset 60

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.

Capabilities — what this component can do (not a risk score):
filesystem readinstall time executionmcp tools detectednetwork egressscoped identityshell execution

Behavioral traits

How this component maps to the CSA agentic threat model. Descriptive — it never affects the risk score.

Tool surface
Tool Usage
Execution authority
Tool Access Control / Direct Tool Access
Filesystem reach
Tool Execution Context
Network egress
Interaction & Communication / Direct Communication
Scoped / least-privilege identity
Tool Execution Context / Least-Privilege Service Identity
Supply-chain provenance risk
General Protections / Supply Chain

Findings (6)

HIGHnpm install-time lifecycle hookST-INSTALL-NPM

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.

HIGHNode.js shell/command executionST-SHELL-NODE

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.

MEDIUMNode.js filesystem readST-FS-NODE-READ

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.

MEDIUMNode.js network egressST-NET-NODE

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.

LOWScoped / least-privilege identityST-AUTH-SCOPED

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 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 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 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.

LOWMCP tool surface detectedST-MCP-DETECTED

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 component

How we determine this: deterministic static analysis (regex + AST), evidence-anchored, no code execution. Methodology →