is-unsafe-permissions
v1.0.0
Published
Audit filesystem and cloud IAM permissions for least-privilege violations — zero dependency core.
Downloads
129
Maintainers
Readme
is-unsafe-permissions
Audit filesystem and cloud IAM permissions for least-privilege violations — zero dependency core.
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-permissionsCloud 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-authorizationQuick 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.jsonimport { 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