mcpField
I
instance-security

instance-security

github.com/serac-labsセキュリティ7758 回インストール9 週間前に更新
リポジトリ

instance-security について

認証、認可、セッション管理、セキュリティ強化を扱う ServiceNow インスタンス向けスキルです。ES5 の例と MCP ツールを使った作業手順を掲載しています。

主な機能

  1. 01パスワードポリシーと MFA の確認
  2. 02セッションセキュリティの確認
  3. 03入力検証とインジェクション対策
  4. 04セキュリティ設定とイベント監査
  5. 05セキュリティ健全性の確認

活用シーン

  1. 01インスタンスのパスワードとセッション設定の確認
  2. 02ACL とセキュリティイベントの調査
  3. 03セキュリティスクリプトと健全性チェックのテスト

MCP 対応

snow_property_manage、snow_execute_script、snow_query_table を使ったセキュリティ設定の確認、スクリプトのテスト、ACL のレビュー方法を説明しています。

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/serac-labs/serac --skill instance-security

インストール後、Claude Code で/instance-securityを入力して読み込まれたか確認します。

詳細

ServiceNow のインスタンスセキュリティ

インスタンスセキュリティでは、認証、認可、セキュリティ強化について扱います。

セキュリティレイヤー

Network Security
    ↓
Authentication (SSO, MFA)
    ↓
Session Management
    ↓
Authorization (ACLs, Roles)
    ↓
Data Protection
    ↓
Audit & Logging

主なテーブル

テーブル目的
sys_userユーザーアカウント
sys_user_roleロール
sys_security_aclアクセス制御
sysevent_logセキュリティイベント
sys_propertiesセキュリティ設定

認証セキュリティ(ES5)

パスワードポリシーの設定
// Check password strength (ES5 ONLY!)
function validatePasswordStrength(password) {
  var issues = []

  // Minimum length
  var minLength = parseInt(gs.getProperty("glide.security.password.min_length", "8"), 10)
  if (password.length < minLength) {
    issues.push("Password must be at least " + minLength + " characters")
  }

  // Complexity requirements
  if (!/[A-Z]/.test(password)) {
    issues.push("Password must contain uppercase letter")
  }
  if (!/[a-z]/.test(password)) {
    issues.push("Password must contain lowercase letter")
  }
  if (!/[0-9]/.test(password)) {
    issues.push("Password must contain number")
  }
  if (!/[!@#$%^&*(),.?":{}|<>]/.test(password)) {
    issues.push("Password must contain special character")
  }

  return {
    valid: issues.length === 0,
    issues: issues,
  }
}
セッションセキュリティ
// Check session validity (ES5 ONLY!)
function isSessionSecure() {
  var session = gs.getSession()

  // Check session age
  var maxAge = parseInt(gs.getProperty("glide.security.session.timeout", "60"), 10)
  var sessionAge = session.getSessionAge() / 60000 // Convert to minutes

  if (sessionAge > maxAge) {
    return { valid: false, reason: "Session expired" }
  }

  // Check for session fixation
  var clientIP = gs.getSession().getClientIP()
  var originalIP = session.getValue("original_ip")

  if (originalIP && clientIP !== originalIP) {
    return { valid: false, reason: "IP address changed" }
  }

  return { valid: true }
}

// Force re-authentication
function requireReauthentication(reason) {
  gs.getSession().invalidate()
  gs.addErrorMessage(reason)
  // Redirect to login
  response.sendRedirect("/login.do")
}

MFA の実装(ES5)

MFA のステータスを確認する
// Check if user has MFA enabled (ES5 ONLY!)
function hasMFAEnabled(userSysId) {
  var user = new GlideRecord("sys_user")
  if (!user.get(userSysId)) {
    return false
  }

  // Check MFA settings
  var mfa = new GlideRecord("sys_user_mfa")
  mfa.addQuery("user", userSysId)
  mfa.addQuery("active", true)
  mfa.query()

  return mfa.hasNext()
}

// Enforce MFA for sensitive operations
function requireMFA(operation) {
  var userId = gs.getUserID()

  if (!hasMFAEnabled(userId)) {
    gs.addErrorMessage("MFA required for " + operation)
    return false
  }

  // Check if MFA verified this session
  var session = gs.getSession()
  var mfaVerified = session.getValue("mfa_verified")

  if (mfaVerified !== "true") {
    // Trigger MFA challenge
    gs.eventQueue("user.mfa.challenge", null, userId, operation)
    return false
  }

  return true
}

入力検証(ES5)

XSS の防止
// Sanitize user input (ES5 ONLY!)
function sanitizeInput(input) {
  if (!input) return ""

  // Encode HTML entities
  var sanitized = input
    .toString()
    .replace(/&/g, "&amp;")
    .replace(/</g, "&lt;")
    .replace(/>/g, "&gt;")
    .replace(/"/g, "&quot;")
    .replace(/'/g, "&#x27;")

  return sanitized
}

// Validate and sanitize before storage
function validateUserInput(tableName, fieldName, value) {
  // Check field type
  var dict = new GlideRecord("sys_dictionary")
  dict.addQuery("name", tableName)
  dict.addQuery("element", fieldName)
  dict.query()

  if (!dict.next()) {
    return { valid: false, error: "Field not found" }
  }

  var fieldType = dict.getValue("internal_type")

  // Validate based on type
  if (fieldType === "string" || fieldType === "html") {
    // Check for script injection
    if (/<script/i.test(value)) {
      return { valid: false, error: "Script tags not allowed" }
    }
    // Check for event handlers
    if (/on\w+\s*=/i.test(value)) {
      return { valid: false, error: "Event handlers not allowed" }
    }
  }

  return { valid: true, sanitized: sanitizeInput(value) }
}
SQL インジェクションの防止
// Safe query building (ES5 ONLY!)
// NEVER concatenate user input directly into queries

// BAD - Vulnerable to injection
// gr.addEncodedQuery('name=' + userInput);

// GOOD - Use parameterized queries
function safeQuery(tableName, fieldName, value) {
  var gr = new GlideRecord(tableName)

  // GlideRecord methods handle escaping
  gr.addQuery(fieldName, value)
  gr.query()

  return gr
}

// For encoded queries, validate input
function safeEncodedQuery(tableName, userQuery) {
  // Whitelist allowed fields
  var allowedFields = ["number", "short_description", "state", "priority"]

  // Parse and validate query
  var parts = userQuery.split("^")
  var safeQuery = []

  for (var i = 0; i < parts.length; i++) {
    var part = parts[i]
    var match = part.match(/^(\w+)(=|!=|LIKE|CONTAINS)(.+)$/)

    if (match) {
      var field = match[1]
      if (allowedFields.indexOf(field) !== -1) {
        safeQuery.push(part)
      }
    }
  }

  var gr = new GlideRecord(tableName)
  if (safeQuery.length > 0) {
    gr.addEncodedQuery(safeQuery.join("^"))
  }
  gr.query()

  return gr
}

セキュリティプロパティ(ES5)

主なセキュリティ設定
// Get security-related properties (ES5 ONLY!)
function getSecuritySettings() {
  return {
    // Session settings
    session_timeout: gs.getProperty("glide.security.session.timeout"),
    session_cookie_secure: gs.getProperty("glide.security.session.cookie.secure"),

    // Password settings
    password_min_length: gs.getProperty("glide.security.password.min_length"),
    password_history: gs.getProperty("glide.security.password.history"),
    password_expiry: gs.getProperty("glide.security.password.expiry"),

    // Login settings
    max_failed_logins: gs.getProperty("glide.security.password.max_failed_logins"),
    lockout_duration: gs.getProperty("glide.security.password.lockout_duration"),

    // General security
    csrf_protection: gs.getProperty("glide.security.csrf.strict_validation"),
    xss_protection: gs.getProperty("glide.security.xss.strict"),
  }
}

// Update security property
function setSecurityProperty(name, value, requireRestart) {
  gs.setProperty(name, value)

  if (requireRestart) {
    gs.warn("Security property changed: " + name + ". Restart may be required.")
  }

  // Log security change
  gs.eventQueue("security.property.changed", null, name, value)
}

セキュリティ監査(ES5)

セキュリティイベントを記録する
// Log security event (ES5 ONLY!)
function logSecurityEvent(eventType, details) {
  var log = new GlideRecord("syslog")
  log.initialize()

  log.setValue("level", "warning")
  log.setValue("source", "Security")
  log.setValue("message", eventType + ": " + JSON.stringify(details))

  log.insert()

  // Also queue for security monitoring
  gs.eventQueue("security.event", null, eventType, JSON.stringify(details))
}

// Track failed login attempts
function trackFailedLogin(username, ipAddress) {
  logSecurityEvent("failed_login", {
    username: username,
    ip: ipAddress,
    timestamp: new GlideDateTime().getDisplayValue(),
  })

  // Check for brute force
  var recentFailures = countRecentFailures(username, 5) // Last 5 minutes
  var maxFailures = parseInt(gs.getProperty("glide.security.password.max_failed_logins", "5"), 10)

  if (recentFailures >= maxFailures) {
    lockAccount(username)
    logSecurityEvent("account_locked", {
      username: username,
      reason: "Too many failed login attempts",
      failures: recentFailures,
    })
  }
}
セキュリティ健全性チェック
// Check instance security health (ES5 ONLY!)
function securityHealthCheck() {
  var issues = []

  // Check for default admin password
  var admin = new GlideRecord("sys_user")
  if (admin.get("user_name", "admin")) {
    if (admin.getValue("password") === gs.getProperty("glide.security.default.admin.password")) {
      issues.push({ severity: "critical", issue: "Default admin password not changed" })
    }
  }

  // Check session timeout
  var timeout = parseInt(gs.getProperty("glide.security.session.timeout", "0"), 10)
  if (timeout === 0 || timeout > 60) {
    issues.push({ severity: "high", issue: "Session timeout too long or disabled" })
  }

  // Check password policy
  var minLength = parseInt(gs.getProperty("glide.security.password.min_length", "0"), 10)
  if (minLength < 12) {
    issues.push({ severity: "medium", issue: "Password minimum length less than 12" })
  }

  // Check HTTPS enforcement
  if (gs.getProperty("glide.security.session.cookie.secure") !== "true") {
    issues.push({ severity: "high", issue: "Secure cookies not enforced" })
  }

  return {
    healthy: issues.length === 0,
    issues: issues,
  }
}

MCP ツールとの連携

利用可能なツール
ツール目的
snow_property_manage (action=get)セキュリティプロパティを確認する
snow_execute_scriptセキュリティスクリプトをテストする
snow_query_tableACL を確認する
ワークフローの例
// 1. Check security properties
await snow_property_manage({
  action: "get",
  name: "glide.security.session.timeout",
})

// 2. Run security health check
await snow_execute_script({
  script: `
        var health = securityHealthCheck();
        gs.info(JSON.stringify(health));
    `,
})

// 3. Review access controls
//    truncate_output is REQUIRED here: `script` is on snow_query_table's
//    truncation list, so without it every ACL script comes back as its first
//    200 characters plus "... [truncated, N chars total]".
await snow_query_table({
  table: "sys_security_acl",
  query: "active=true^admin_overrides=true",
  fields: ["name", "operation", "type", "script"],
  truncate_output: false,
})

切り詰めルールを一切適用せずに ACL スクリプトを 1 つすべて読み取るには、 snow_get_by_sysid({ table: "sys_security_acl", sys_id }) を使用します。これは切り詰めを適用しません。Table API の読み取りで返される内容と返されない内容の詳細については、 table-api-reads スキルを参照してください。

ベストプラクティス

  1. 強力なパスワード - 複雑さを必須にします
  2. MFA - 特権ユーザーに対して有効にします
  3. セッションタイムアウト - 15~30 分
  4. 入力検証 - すべてをサニタイズします
  5. 最小権限 - ロールを最小限にします
  6. 監査ログ - セキュリティイベントを記録します
  7. 定期的なレビュー - セキュリティ評価を実施します
  8. ES5 のみ - 最新の JavaScript 構文は使用しません

コメント