Skip to content

NaturalIntelligence/is-unsafe-permissions

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

2 Commits
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

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

About

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

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors