{
  "name": "Sentinel weekly access review",
  "nodes": [
    {
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "weeks",
              "weeksInterval": 1,
              "triggerAtDay": [1],
              "triggerAtHour": 8
            }
          ]
        }
      },
      "id": "f6c7b1c5-2d8a-4a7f-816c-0d1e2f3a4b56",
      "name": "Monday 08:00",
      "type": "n8n-nodes-base.scheduleTrigger",
      "typeVersion": 1.2,
      "position": [0, 0],
      "notes": "Change to a daily interval if your auditor wants nightly evidence."
    },
    {
      "parameters": {
        "url": "https://sentinel.rootstuff.io/api/v1/users",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpBearerAuth",
        "options": {}
      },
      "id": "a7d8c2d6-3e9b-4b80-927d-1e2f3a4b5c67",
      "name": "Team members",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [260, 0],
      "notes": "Bearer Auth credential holding a Sentinel API token (User Settings, API Tokens). Read permission is enough."
    },
    {
      "parameters": {
        "jsCode": "const team = $input.first().json;\nconst now = Date.now();\nconst staleAfterDays = 90;\nconst stale = staleAfterDays * 24 * 60 * 60 * 1000;\nconst lines = [];\n\nfor (const member of team.data) {\n  const flags = [];\n  if (!member.mfa_enabled) flags.push('no MFA');\n  if (member.status !== 'active') flags.push('disabled');\n  if (!member.last_login_at) flags.push('never signed in');\n  else if (now - Date.parse(member.last_login_at) > stale) flags.push(`no sign-in in ${staleAfterDays} days`);\n  if (flags.length) lines.push(`• ${member.name} <${member.email}> (${member.role}): ${flags.join(', ')}`);\n}\n\nconst text = lines.length\n  ? `Sentinel access review for *${team.team}*: ${lines.length} of ${team.total} members need a look\\n${lines.join('\\n')}`\n  : `Sentinel access review for *${team.team}*: all ${team.total} members have MFA on and signed in within ${staleAfterDays} days`;\n\nreturn [{ json: { text, flagged: lines.length, total: team.total, members: team.data } }];"
      },
      "id": "b8e9d3e7-4f0c-4c91-a38e-2f3a4b5c6d78",
      "name": "Flag members",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [520, 0],
      "notes": "Edit staleAfterDays or the flags to match your access policy."
    },
    {
      "parameters": {
        "select": "channel",
        "channelId": {
          "__rl": true,
          "mode": "name",
          "value": "#security"
        },
        "text": "={{ $json.text }}",
        "otherOptions": {}
      },
      "id": "c9f0e4f8-5a1d-4da2-b49f-3a4b5c6d7e89",
      "name": "Post review",
      "type": "n8n-nodes-base.slack",
      "typeVersion": 2.3,
      "position": [780, 0],
      "notes": "Swap for email, Google Sheets or a ticket if the evidence needs to land somewhere other than a channel."
    }
  ],
  "connections": {
    "Monday 08:00": {
      "main": [
        [
          {
            "node": "Team members",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Team members": {
      "main": [
        [
          {
            "node": "Flag members",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Flag members": {
      "main": [
        [
          {
            "node": "Post review",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "settings": {
    "executionOrder": "v1"
  },
  "active": false,
  "pinData": {},
  "meta": {
    "templateCredsSetupCompleted": false
  }
}
