hana-cli
Version:
HANA Developer Command Line Interface
387 lines (334 loc) • 10.7 kB
JavaScript
// @ts-check
import * as baseLite from '../utils/base-lite.js'
import { buildDocEpilogue } from '../utils/doc-linker.js'
export const command = 'grantChains'
export const aliases = ['grants', 'grantchain']
export const describe = baseLite.bundle.getText("grantChains")
const grantChainsOptions = {
user: {
alias: ['u'],
type: 'string',
desc: baseLite.bundle.getText("targetUser")
},
role: {
alias: ['r'],
type: 'string',
desc: baseLite.bundle.getText("targetRole")
},
depth: {
alias: ['d'],
type: 'number',
default: 5,
desc: baseLite.bundle.getText("chainDepth")
},
format: {
alias: ['f'],
type: 'string',
choices: ['tree', 'table', 'json'],
default: 'tree',
desc: baseLite.bundle.getText("outputFormat")
}
}
export const builder = (yargs) => yargs.options(baseLite.getBuilder(grantChainsOptions)).wrap(160).example('hana-cli grantChains --user DBUSER', baseLite.bundle.getText("grantChainsExample")).wrap(160).epilog(buildDocEpilogue('grantChains', 'security', ['privilegeAnalysis', 'privilegeError', 'roles']))
export const grantChainsBuilderOptions = baseLite.getBuilder(grantChainsOptions)
export let inputPrompts = {
user: {
description: baseLite.bundle.getText("targetUser"),
type: 'string',
required: false
},
depth: {
description: baseLite.bundle.getText("chainDepth"),
type: 'number',
required: false
}
}
/**
* Command handler function
* @param {object} argv - Command line arguments from yargs
* @returns {Promise<void>}
*/
export async function handler(argv) {
const base = await import('../utils/base.js')
base.promptHandler(argv, visualizeGrantChains, inputPrompts, true, true, grantChainsBuilderOptions)
}
/**
* Visualize privilege inheritance chains
* @param {object} prompts - Input prompts
* @returns {Promise<void>}
*/
export async function visualizeGrantChains(prompts) {
const base = await import('../utils/base.js')
base.debug('visualizeGrantChains')
try {
base.setPrompts(prompts)
const db = await base.createDBConnection()
try {
const targetUser = prompts.user || null
const targetRole = prompts.role || null
const maxDepth = prompts.depth || 5
const format = prompts.format || 'tree'
base.output('')
base.output(base.colors.bold(base.bundle.getText('grantChainsHeader')))
base.output('')
if (targetUser) {
await analyzeUserGrantChain(db, targetUser, maxDepth, format, base)
} else if (targetRole) {
await analyzeRoleGrantChain(db, targetRole, maxDepth, format, base)
} else {
// Analyze current user
const currentUserQuery = `SELECT CURRENT_USER FROM DUMMY`
const currentUser = await db.execSQL(currentUserQuery)
if (currentUser && currentUser.length > 0) {
await analyzeUserGrantChain(db, currentUser[0].CURRENT_USER, maxDepth, format, base)
}
}
await base.end()
} catch (error) {
await base.error(error)
}
} catch (error) {
await base.error(error)
}
}
/**
* Analyze grant chain for a specific user
*/
async function analyzeUserGrantChain(db, userName, maxDepth, format, base) {
base.output(base.colors.bold(base.bundle.getText('analyzingGrantChain', [userName])))
base.output('')
// Get roles granted to user
const userRolesQuery = `
SELECT
ROLE_NAME,
GRANTOR,
IS_GRANTABLE
FROM SYS.GRANTED_ROLES
WHERE GRANTEE = ?
ORDER BY ROLE_NAME
`
const userRoles = await db.statementExecPromisified(
await db.preparePromisified(userRolesQuery),
[userName]
)
if (!userRoles || userRoles.length === 0) {
base.output(base.bundle.getText('noRolesGranted'))
base.output('')
return
}
// Build the full grant chain
const grantChain = {
name: userName,
type: 'USER',
roles: []
}
for (const role of userRoles) {
const roleChain = await buildRoleChain(db, role.ROLE_NAME, 1, maxDepth, base)
roleChain.grantor = role.GRANTOR
roleChain.isGrantable = role.IS_GRANTABLE
grantChain.roles.push(roleChain)
}
// Display based on format
if (format === 'tree') {
displayTreeFormat(grantChain, base)
} else if (format === 'table') {
displayTableFormat(grantChain, base)
} else if (format === 'json') {
base.output(JSON.stringify(grantChain, null, 2))
}
base.output('')
displayGrantChainSummary(grantChain, base)
}
/**
* Build role inheritance chain recursively
*/
async function buildRoleChain(db, roleName, currentDepth, maxDepth, base) {
const roleInfo = {
name: roleName,
type: 'ROLE',
depth: currentDepth,
privileges: [],
nestedRoles: []
}
// Get privileges directly granted to this role
const privilegesQuery = `
SELECT
OBJECT_TYPE,
SCHEMA_NAME,
OBJECT_NAME,
PRIVILEGE
FROM SYS.GRANTED_PRIVILEGES
WHERE GRANTEE = ?
ORDER BY OBJECT_TYPE, SCHEMA_NAME, OBJECT_NAME, PRIVILEGE
`
const privileges = await db.statementExecPromisified(
await db.preparePromisified(privilegesQuery),
[roleName]
)
roleInfo.privileges = privileges || []
// Get nested roles if we haven't reached max depth
if (currentDepth < maxDepth) {
const nestedRolesQuery = `
SELECT ROLE_NAME, GRANTOR, IS_GRANTABLE
FROM GRANTED_ROLES
WHERE GRANTEE = ?
ORDER BY ROLE_NAME
`
const nestedRoles = await db.statementExecPromisified(
await db.preparePromisified(nestedRolesQuery),
[roleName]
)
if (nestedRoles && nestedRoles.length > 0) {
for (const nested of nestedRoles) {
const nestedChain = await buildRoleChain(db, nested.ROLE_NAME, currentDepth + 1, maxDepth, base)
nestedChain.grantor = nested.GRANTOR
nestedChain.isGrantable = nested.IS_GRANTABLE
roleInfo.nestedRoles.push(nestedChain)
}
}
}
return roleInfo
}
/**
* Display grant chain in tree format
*/
function displayTreeFormat(grantChain, base) {
base.output(base.colors.cyan.bold(`📦 ${grantChain.type}: ${grantChain.name}`))
base.output('')
for (const role of grantChain.roles) {
displayRoleTree(role, 0, base)
}
}
/**
* Display role tree recursively
*/
function displayRoleTree(role, indent, base) {
const indentStr = ' '.repeat(indent)
const grantableStr = role.isGrantable === 'TRUE' ? base.colors.yellow(' [GRANTABLE]') : ''
base.output(`${indentStr}├─ ${base.colors.blue('ROLE:')} ${role.name}${grantableStr}`)
if (role.grantor) {
base.output(`${indentStr}│ ${base.colors.gray(`Granted by: ${role.grantor}`)}`)
}
// Show privileges
if (role.privileges && role.privileges.length > 0) {
const privCount = role.privileges.length
base.output(`${indentStr}│ ${base.colors.green(`${privCount} privilege(s):`)}`)
// Group by object type
const privsByType = {}
for (const priv of role.privileges) {
if (!privsByType[priv.OBJECT_TYPE]) {
privsByType[priv.OBJECT_TYPE] = []
}
privsByType[priv.OBJECT_TYPE].push(priv)
}
for (const [objType, privs] of Object.entries(privsByType)) {
if (privs.length <= 3) {
for (const priv of privs) {
const fullName = priv.SCHEMA_NAME ? `${priv.SCHEMA_NAME}.${priv.OBJECT_NAME}` : priv.OBJECT_NAME
base.output(`${indentStr}│ • ${priv.PRIVILEGE} on ${objType} ${fullName}`)
}
} else {
base.output(`${indentStr}│ • ${privs.length} ${objType} privileges`)
}
}
}
// Show nested roles
if (role.nestedRoles && role.nestedRoles.length > 0) {
base.output(`${indentStr}│`)
for (const nested of role.nestedRoles) {
displayRoleTree(nested, indent + 1, base)
}
}
base.output(`${indentStr}│`)
}
/**
* Display grant chain in table format
*/
function displayTableFormat(grantChain, base) {
const flattenedGrants = []
for (const role of grantChain.roles) {
flattenGrants(role, grantChain.name, '', flattenedGrants)
}
if (flattenedGrants.length > 0) {
base.output(base.colors.bold(base.bundle.getText('grantChainTable')))
base.outputTableFancy(flattenedGrants)
}
}
/**
* Flatten grant chain for table display
*/
function flattenGrants(role, owner, path, result) {
const currentPath = path ? `${path} → ${role.name}` : role.name
result.push({
'Grantee': owner,
'Path': currentPath,
'Type': role.type,
'Privileges': role.privileges ? role.privileges.length : 0,
'Grantable': role.isGrantable === 'TRUE' ? 'Yes' : 'No',
'Depth': role.depth || 0
})
if (role.nestedRoles && role.nestedRoles.length > 0) {
for (const nested of role.nestedRoles) {
flattenGrants(nested, owner, currentPath, result)
}
}
}
/**
* Analyze grant chain for a specific role
*/
async function analyzeRoleGrantChain(db, roleName, maxDepth, format, base) {
base.output(base.colors.bold(base.bundle.getText('analyzingRoleChain', [roleName])))
base.output('')
const roleChain = await buildRoleChain(db, roleName, 0, maxDepth, base)
if (format === 'tree') {
displayRoleTree(roleChain, 0, base)
} else if (format === 'table') {
const flattenedGrants = []
flattenGrants(roleChain, roleName, '', flattenedGrants)
if (flattenedGrants.length > 0) {
base.outputTableFancy(flattenedGrants)
}
} else if (format === 'json') {
base.output(JSON.stringify(roleChain, null, 2))
}
base.output('')
}
/**
* Display grant chain summary
*/
function displayGrantChainSummary(grantChain, base) {
let totalRoles = 0
let totalPrivileges = 0
let maxDepth = 0
let grantableCount = 0
function countChain(role, depth) {
totalRoles++
if (role.privileges) {
totalPrivileges += role.privileges.length
}
if (depth > maxDepth) {
maxDepth = depth
}
if (role.isGrantable === 'TRUE') {
grantableCount++
}
if (role.nestedRoles && role.nestedRoles.length > 0) {
for (const nested of role.nestedRoles) {
countChain(nested, depth + 1)
}
}
}
for (const role of grantChain.roles) {
countChain(role, 1)
}
base.output(base.colors.bold(base.bundle.getText('grantChainSummary')))
base.output(` ${base.bundle.getText('totalRolesInChain')}: ${totalRoles}`)
base.output(` ${base.bundle.getText('totalPrivileges')}: ${totalPrivileges}`)
base.output(` ${base.bundle.getText('maxChainDepth')}: ${maxDepth}`)
base.output(` ${base.bundle.getText('grantableRoles')}: ${grantableCount}`)
base.output('')
if (grantableCount > 0) {
base.output(base.colors.yellow(`⚠️ ${base.bundle.getText('grantableRolesWarning')}`))
base.output('')
}
}