npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

is-unsafe-permissions

v1.0.0

Published

Audit filesystem and cloud IAM permissions for least-privilege violations — zero dependency core.

Downloads

129

Readme

is-unsafe-permissions

Audit filesystem and cloud IAM permissions for least-privilege violations — zero dependency core.

zero-dep core ESM + CJS TypeScript


Philosophy

Two surfaces, one coherent API. Filesystem checks use only Node.js built-ins (zero deps). Cloud adapters are separate entry points that peer-dep on the relevant SDK — you only pay for what you import. All results share the same PermissionResult shape with fully discriminated violation types.


Install

Core (filesystem, zero deps)

npm install is-unsafe-permissions

Cloud adapters (optional — install the peer deps you need)

# AWS
npm install is-unsafe-permissions @aws-sdk/client-iam @aws-sdk/client-s3

# GCP
npm install is-unsafe-permissions @google-cloud/iam

# Azure
npm install is-unsafe-permissions @azure/arm-authorization

Quick start

Filesystem

import { checkFsPerms } from 'is-unsafe-permissions'

// Single file — baseline world-writable check
const result = await checkFsPerms('/home/user/.ssh/id_rsa')
// { unsafe: false, violations: [] }

// With a built-in rule set
const result = await checkFsPerms('/home/user/.ssh/id_rsa', { ruleSet: 'ssh-keys' })
// {
//   unsafe: true,
//   violations: [
//     { type: 'fs', rule: 'world-readable', severity: 'high',
//       path: '/home/user/.ssh/id_rsa', actual: '0644', expected: '0600', detail: '...' }
//   ]
// }

// Recursive directory scan
const result = await checkFsPerms('/etc/ssl', {
  recursive: true,
  ruleSet:   'certs',
  depth:     3,
})

Cloud — policy-only lint (no SDK required)

import { aws } from 'is-unsafe-permissions/aws'

const result = await aws.checkPolicy({
  Statement: [{ Effect: 'Allow', Action: '*', Resource: '*' }],
})
// {
//   unsafe: true,
//   violations: [
//     { type: 'cloud', rule: 'wildcard-action',   severity: 'critical', resource: '*', detail: '...' },
//     { type: 'cloud', rule: 'wildcard-resource',  severity: 'critical', resource: '*', detail: '...' },
//   ]
// }

API

checkFsPerms(path, options?)

import { checkFsPerms } from 'is-unsafe-permissions'

| Option | Type | Default | Description | |---|---|---|---| | ruleSet | FsRuleSet | — | Named rule set to apply (see table below) | | expect | string \| (stat, path) => string | — | Expected octal mode, e.g. '0600' | | recursive | boolean | false | Recursively scan directories | | depth | number | Infinity | Max recursion depth | | followSymlinks | boolean | false | Follow symbolic links | | checkSuid | boolean | true | Check for SUID/SGID bits | | suidAllowlist | string[] | built-in list | Paths exempt from SUID/SGID violations |

// Custom expected mode — string
await checkFsPerms('/var/myapp/.env', { expect: '0600' })

// Custom expected mode — callback (dynamic rules per file)
await checkFsPerms('/etc/ssl', {
  recursive: true,
  expect: (stat, filePath) => filePath.endsWith('.key') ? '0600' : '0644',
})

// SUID allowlist
await checkFsPerms('/usr/local/bin', {
  recursive: true,
  suidAllowlist: ['/usr/local/bin/my-suid-tool'],
})

Windows: Returns a platform-unsupported violation with severity: 'low' instead of throwing.


Cloud — AWS

import { aws } from 'is-unsafe-permissions/aws'
// or named imports:
import { checkPolicy, checkBucket, checkRole } from 'is-unsafe-permissions/aws'

aws.checkPolicy(policyDoc)

Lint a raw IAM policy document (object or JSON string). Useful for Terraform/CloudFormation CI.

aws.checkBucket(bucketName, { s3Client, checks? }) — live

import { S3Client } from '@aws-sdk/client-s3'

const result = await aws.checkBucket('my-bucket', {
  s3Client: new S3Client({ region: 'us-east-1' }),
  checks: ['public-acl', 'encryption', 'versioning', 'mfa-delete', 'public-block'],
})

aws.checkRole(roleArnOrName, { iamClient }) — live

Fetches and lints the trust policy plus all attached and inline policies.

import { IAMClient } from '@aws-sdk/client-iam'

const result = await aws.checkRole('arn:aws:iam::123456789:role/MyRole', {
  iamClient: new IAMClient({ region: 'us-east-1' }),
})

Cloud — GCP

import { gcp } from 'is-unsafe-permissions/gcp'

gcp.checkBinding(bindingOrPolicy)

Accepts a single binding { role, members } or a full policy with a bindings array.

gcp.checkStorageBucket(bucketConfig)

Checks for public access and missing uniform bucket-level access.


Cloud — Azure

import { azure } from 'is-unsafe-permissions/azure'

azure.checkRoleAssignment(assignmentOrList)

Accepts a single role assignment, an array, or a JSON string.

azure.checkStorageAccount(storageConfig)

Checks for public blob access, shared key access, and HTTPS enforcement.


Result types

PermissionResult

interface PermissionResult {
  unsafe:     boolean
  violations: PermissionViolation[]   // FsViolation | CloudViolation
  error?:     PermissionCheckError    // present when the check itself failed
}

Discriminated union — FsViolation | CloudViolation

All violations are tagged with a type field. This discriminates path (always present on 'fs') from resource (always present on 'cloud'), so TypeScript narrows correctly in a switch/if-else without optional chaining.

// FsViolation — from checkFsPerms
interface FsViolation {
  type:      'fs'
  rule:      FsRule              // autocomplete-friendly; accepts custom strings too
  severity:  Severity
  path:      string              // always present
  resource?: never               // never present — narrows cleanly
  actual?:   string
  expected?: string
  detail?:   string
}

// CloudViolation — from any cloud adapter
interface CloudViolation {
  type:     'cloud'
  rule:     AwsRule | GcpRule | AzureRule   // autocomplete-friendly
  severity: Severity
  resource: string               // always present
  path?:    never                // never present
  actual?:  string
  expected?: string
  detail?:  string
}

Example narrowing:

import { checkFsPerms, SEVERITY_WEIGHTS } from 'is-unsafe-permissions'
import { aws } from 'is-unsafe-permissions/aws'

const results = await Promise.all([
  checkFsPerms('/etc/ssl', { recursive: true, ruleSet: 'certs' }),
  aws.checkPolicy(policyDoc),
])

for (const { violations } of results) {
  for (const v of violations) {
    if (v.type === 'fs') {
      console.log(v.path)      // string — no ?. needed
    } else {
      console.log(v.resource)  // string — no ?. needed
    }
  }
}

SEVERITY_WEIGHTS

A numeric weight map for sorting violations or setting CI thresholds.

import { SEVERITY_WEIGHTS } from 'is-unsafe-permissions'
// { critical: 4, high: 3, medium: 2, low: 1 }

// Sort highest severity first
violations.sort((a, b) => SEVERITY_WEIGHTS[b.severity] - SEVERITY_WEIGHTS[a.severity])

// Keep only high and above
const important = violations.filter(v => SEVERITY_WEIGHTS[v.severity] >= SEVERITY_WEIGHTS.high)

PermissionCheckError

Thrown (rather than silently swallowed) when a check encounters an operational problem — missing SDK, API throttling, malformed JSON, etc.

import { PermissionCheckError, ErrorCode } from 'is-unsafe-permissions'

try {
  await aws.checkBucket('my-bucket', { s3Client })
} catch (err) {
  if (err instanceof PermissionCheckError) {
    switch (err.code) {
      case ErrorCode.RESOURCE_NOT_FOUND: // bucket doesn't exist
      case ErrorCode.PERMISSION_DENIED:  // IAM doesn't allow GetBucketAcl
      case ErrorCode.RATE_LIMITED:       // API throttled
      case ErrorCode.SDK_NOT_FOUND:      // forgot to install peer dep
      case ErrorCode.INVALID_POLICY:     // malformed JSON passed to checkPolicy
      case ErrorCode.TIMEOUT:
      case ErrorCode.NETWORK_ERROR:
      case ErrorCode.STAT_ERROR:         // fs.lstat failed (checkFsPerms only)
    }
    console.error(err.cause) // original SDK/fs error is preserved
  }
}

All error codes are available as ErrorCode.* constants. The cause field always holds the original underlying error when one exists.


Typed rule identifiers

Every rule string is typed as a specific literal union, which means your editor autocompletes rule names and catches typos at compile time. The (string & {}) escape hatch means custom rule strings from user-defined checks still type-check without casting.

import type { FsRule, AwsRule, GcpRule, AzureRule, Rule } from 'is-unsafe-permissions'

// Filter to only critical AWS violations by rule
const wildcards = violations.filter(
  (v): v is CloudViolation => v.type === 'cloud' &&
    (v.rule === 'wildcard-action' || v.rule === 'wildcard-resource')
)

Built-in filesystem rule sets

| Rule set | Description | |---|---| | ssh-keys | Expect 0600; flag anything looser | | env-files | .env, .env.local → expect 0600 | | certs | Public certs 0644 ok, private keys must be 0600 | | shared-dirs | /tmp-style dirs must have the sticky bit | | strict | Flags anything world-accessible (read, write, or execute) | | web-root | HTML/assets 0644 ok, no exec bits on content files | | log-dirs | Writable by owner/group only, not world |


Cloud violation rules

AWS

| Rule | Severity | Description | |---|---|---| | wildcard-action | critical | Action: * — grants all actions | | wildcard-resource | critical | Resource: * — applies to all resources | | notaction-wildcard-resource | critical | NotAction + Resource: * — effectively wildcard | | sensitive-action-no-condition | high | IAM/KMS/STS actions with no Condition block | | cross-account-trust-no-condition | high | Cross-account trust without ExternalId or MFA condition | | public-s3-acl | critical | Bucket ACL grants access to AllUsers or AuthenticatedUsers | | unencrypted-s3-bucket | medium | No server-side encryption configured | | no-mfa-delete | medium | Versioned bucket without MFA Delete | | public-access-block-incomplete | high | Missing one or more S3 Block Public Access settings |

GCP

| Rule | Severity | Description | |---|---|---| | primitive-role-on-non-admin | high | roles/owner or roles/editor on a non-service-account member | | all-users-binding | critical | Any role granted to allUsers | | all-authenticated-users-binding | high | Any role granted to allAuthenticatedUsers | | public-gcs-bucket | critical | allUsers in a bucket IAM binding | | missing-uniform-bucket-level-access | medium | Uniform bucket-level access not enabled |

Azure

| Rule | Severity | Description | |---|---|---| | broad-role-subscription-scope | high | Owner or Contributor at subscription scope | | owner-role-assigned | high | Owner role assigned at any scope | | public-blob-access | critical | allowBlobPublicAccess: true on storage account | | shared-key-access-enabled | medium | Shared Key auth not disabled | | http-traffic-allowed | high | supportsHttpsTrafficOnly not enforced |


CI recipe — fail on critical violations

import { checkFsPerms, SEVERITY_WEIGHTS } from 'is-unsafe-permissions'
import { aws } from 'is-unsafe-permissions/aws'

async function ciCheck() {
  const [fsResult, iamResult] = await Promise.all([
    checkFsPerms('/etc/ssl', { recursive: true, ruleSet: 'certs' }),
    aws.checkPolicy(JSON.parse(readFileSync('./infra/iam-policy.json', 'utf8'))),
  ])

  const all = [...fsResult.violations, ...iamResult.violations]
  const criticals = all.filter(v => SEVERITY_WEIGHTS[v.severity] >= SEVERITY_WEIGHTS.critical)

  if (criticals.length > 0) {
    console.error('Critical permission violations:')
    criticals.forEach(v => console.error(
      `  [${v.severity.toUpperCase()}] ${v.rule} — ${'path' in v ? v.path : v.resource}\n    ${v.detail}`
    ))
    process.exit(1)
  }
}

ciCheck()

Policy-lint recipe — pipe Terraform JSON into aws.checkPolicy

terraform plan -out=plan.tfplan
terraform show -json plan.tfplan > plan.json
import { aws } from 'is-unsafe-permissions/aws'
import { PermissionCheckError } from 'is-unsafe-permissions'
import { readFileSync } from 'fs'

const plan = JSON.parse(readFileSync('plan.json', 'utf8'))

const policies = plan.resource_changes
  .filter(r => r.type === 'aws_iam_policy')
  .map(r => JSON.parse(r.change.after.policy))

for (const policy of policies) {
  try {
    const result = await aws.checkPolicy(policy)
    if (result.unsafe) {
      result.violations.forEach(v => console.error(`[${v.severity}] ${v.rule}: ${v.detail}`))
      process.exit(1)
    }
  } catch (err) {
    if (err instanceof PermissionCheckError && err.code === 'INVALID_POLICY') {
      console.error('Malformed policy JSON in plan:', err.message)
      process.exit(2)
    }
    throw err
  }
}

Platform notes

POSIX only (Linux / macOS) for filesystem checks. On Windows, checkFsPerms returns a single { type: 'fs', rule: 'platform-unsupported', severity: 'low' } violation rather than throwing. Cloud adapters work on all platforms.


Source layout

src/
  index.js                ← checkFsPerms, SEVERITY_WEIGHTS, PermissionCheckError
  types.d.ts              ← all TypeScript types
  errors.js               ← PermissionCheckError, ErrorCode, classifyCloudError
  fs/
    check.js              ← checkFsPerms() — Node.js fs only, zero dep
    rules/
      mode.js             ← octal mode parser and comparator
      rule-sets.js        ← built-in named rule sets
      suid.js             ← SUID/SGID detection + allowlist
  cloud/
    aws/
      index.js            ← aws entry point
      policy.js           ← policy-only static analysis (zero dep)
      bucket.js           ← S3 live checks (peer-dep: @aws-sdk/client-s3)
      role.js             ← IAM live checks (peer-dep: @aws-sdk/client-iam)
    gcp/
      index.js            ← gcp entry point
    azure/
      index.js            ← azure entry point

specs/
  fs-mode.spec.js
  fs-check.spec.js
  fs-recursive.spec.js
  fs-rule-sets.spec.js
  fs-suid.spec.js
  cloud-aws-policy.spec.js
  cloud-gcp.spec.js
  cloud-azure.spec.js
  result-type.spec.js
  platform.spec.js