From 493608f865fba9bf6631f5f047a59b3032e78293 Mon Sep 17 00:00:00 2001 From: Chris Brown <1731074+ccbrown@users.noreply.github.com> Date: Mon, 21 Apr 2025 18:19:08 -0400 Subject: [PATCH] add syntax highlighting and new article on tag-based permissions --- frontend/package-lock.json | 287 ++++++++++++++++++ frontend/package.json | 2 + frontend/public/images/chris.jpg | Bin 0 -> 18056 bytes .../(public-area)/articles/[slug]/page.tsx | 39 ++- .../articles/capital-one-data-breach.tsx | 4 + ...s-to-modify-only-resources-they-create.tsx | 235 ++++++++++++++ .../src/app/(public-area)/articles/index.tsx | 6 + .../src/app/(public-area)/articles/page.tsx | 28 +- frontend/src/app/(public-area)/page.tsx | 11 +- .../app/(user-area)/teams/[teamId]/Rules.tsx | 11 +- frontend/src/app/globals.css | 5 + frontend/src/components/Markdown.tsx | 2 +- frontend/src/components/SyntaxHighlighter.tsx | 26 ++ frontend/src/components/index.tsx | 1 + 14 files changed, 630 insertions(+), 27 deletions(-) create mode 100644 frontend/public/images/chris.jpg create mode 100644 frontend/src/app/(public-area)/articles/how-to-allow-aws-principals-to-modify-only-resources-they-create.tsx create mode 100644 frontend/src/components/SyntaxHighlighter.tsx diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 74901c60..b05a96c5 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -16,6 +16,7 @@ "@rematch/loading": "^2.1.2", "@stripe/react-stripe-js": "^3.5.1", "@stripe/stripe-js": "^6.1.0", + "@types/react-syntax-highlighter": "^15.5.13", "@types/three": "^0.174.0", "clsx": "^2.1.1", "immer": "^10.1.1", @@ -29,6 +30,7 @@ "react-map-gl": "^8.0.1", "react-markdown": "^10.1.0", "react-redux": "^9.2.0", + "react-syntax-highlighter": "^15.6.1", "redux": "^5.0.1", "three": "^0.174.0" }, @@ -3922,6 +3924,15 @@ "@types/react": "*" } }, + "node_modules/@types/react-syntax-highlighter": { + "version": "15.5.13", + "resolved": "https://registry.npmjs.org/@types/react-syntax-highlighter/-/react-syntax-highlighter-15.5.13.tgz", + "integrity": "sha512-uLGJ87j6Sz8UaBAooU0T6lWJ0dBmjZgN1PZTrj05TNql2/XpC6+4HhMT5syIdFUUt+FASfCeLLv4kBygNU+8qA==", + "license": "MIT", + "dependencies": { + "@types/react": "*" + } + }, "node_modules/@types/stats.js": { "version": "0.17.3", "resolved": "https://registry.npmjs.org/@types/stats.js/-/stats.js-0.17.3.tgz", @@ -6706,6 +6717,19 @@ "reusify": "^1.0.4" } }, + "node_modules/fault": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/fault/-/fault-1.0.4.tgz", + "integrity": "sha512-CJ0HCB5tL5fYTEA7ToAq5+kTwd++Borf1/bifxd9iT70QcXr4MRrO3Llf8Ifs70q+SJcGHFtnIE/Nw6giCtECA==", + "license": "MIT", + "dependencies": { + "format": "^0.2.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/fflate": { "version": "0.8.2", "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz", @@ -6855,6 +6879,14 @@ "node": ">= 6" } }, + "node_modules/format": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/format/-/format-0.2.2.tgz", + "integrity": "sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww==", + "engines": { + "node": ">=0.4.x" + } + }, "node_modules/fs-extra": { "version": "10.1.0", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", @@ -7303,6 +7335,16 @@ "node": ">= 0.4" } }, + "node_modules/hast-util-parse-selector": { + "version": "2.2.5", + "resolved": "https://registry.npmjs.org/hast-util-parse-selector/-/hast-util-parse-selector-2.2.5.tgz", + "integrity": "sha512-7j6mrk/qqkSehsM92wQjdIgWM2/BW61u/53G6xmC8i1OmEdKLHbk419QKQUjz6LglWsfqoiHmyMRkP1BGjecNQ==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/hast-util-to-jsx-runtime": { "version": "2.3.6", "resolved": "https://registry.npmjs.org/hast-util-to-jsx-runtime/-/hast-util-to-jsx-runtime-2.3.6.tgz", @@ -7343,6 +7385,86 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/hastscript": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/hastscript/-/hastscript-6.0.0.tgz", + "integrity": "sha512-nDM6bvd7lIqDUiYEiu5Sl/+6ReP0BMk/2f4U/Rooccxkj0P5nm+acM5PrGJ/t5I8qPGiqZSE6hVAwZEdZIvP4w==", + "license": "MIT", + "dependencies": { + "@types/hast": "^2.0.0", + "comma-separated-tokens": "^1.0.0", + "hast-util-parse-selector": "^2.0.0", + "property-information": "^5.0.0", + "space-separated-tokens": "^1.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hastscript/node_modules/@types/hast": { + "version": "2.3.10", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-2.3.10.tgz", + "integrity": "sha512-McWspRw8xx8J9HurkVBfYj0xKoE25tOFlHGdx4MJ5xORQrMGZNqJhVQWaIbm6Oyla5kYOXtDiopzKRJzEOkwJw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2" + } + }, + "node_modules/hastscript/node_modules/@types/unist": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz", + "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==", + "license": "MIT" + }, + "node_modules/hastscript/node_modules/comma-separated-tokens": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-1.0.8.tgz", + "integrity": "sha512-GHuDRO12Sypu2cV70d1dkA2EUmXHgntrzbpvOB+Qy+49ypNfGgFQIC2fhhXbnyrJRynDCAARsT7Ou0M6hirpfw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/hastscript/node_modules/property-information": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/property-information/-/property-information-5.6.0.tgz", + "integrity": "sha512-YUHSPk+A30YPv+0Qf8i9Mbfe/C0hdPXk1s1jPVToV8pk8BQtpw10ct89Eo7OWkutrwqvT0eicAxlOg3dOAu8JA==", + "license": "MIT", + "dependencies": { + "xtend": "^4.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/hastscript/node_modules/space-separated-tokens": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-1.1.5.tgz", + "integrity": "sha512-q/JSVd1Lptzhf5bkYm4ob4iWPjx0KiRe3sRFBNrVqbJkFaBm5vbbowy1mymoPNLRa52+oadOhJ+K49wsSeSjTA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/highlight.js": { + "version": "10.7.3", + "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-10.7.3.tgz", + "integrity": "sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==", + "license": "BSD-3-Clause", + "engines": { + "node": "*" + } + }, + "node_modules/highlightjs-vue": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/highlightjs-vue/-/highlightjs-vue-1.0.0.tgz", + "integrity": "sha512-PDEfEF102G23vHmPhLyPboFCD+BkMGu+GuJe2d9/eH4FsCwvgBpnc9n0pGE+ffKdph38s6foEZiEjdgHdzp+IA==", + "license": "CC0-1.0" + }, "node_modules/html-url-attributes": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/html-url-attributes/-/html-url-attributes-3.0.1.tgz", @@ -8633,6 +8755,20 @@ "tslib": "^2.0.3" } }, + "node_modules/lowlight": { + "version": "1.20.0", + "resolved": "https://registry.npmjs.org/lowlight/-/lowlight-1.20.0.tgz", + "integrity": "sha512-8Ktj+prEb1RoCPkEOrPMYUN/nCggB7qAWe3a7OpMjWQkh3l2RD5wKRQ+o8Q8YuI9RG/xs95waaI/E6ym/7NsTw==", + "license": "MIT", + "dependencies": { + "fault": "^1.0.0", + "highlight.js": "~10.7.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/lru-cache": { "version": "10.4.3", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", @@ -10112,6 +10248,15 @@ "node": ">=6.0.0" } }, + "node_modules/prismjs": { + "version": "1.30.0", + "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.30.0.tgz", + "integrity": "sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/prop-types": { "version": "15.8.1", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", @@ -10329,6 +10474,23 @@ } } }, + "node_modules/react-syntax-highlighter": { + "version": "15.6.1", + "resolved": "https://registry.npmjs.org/react-syntax-highlighter/-/react-syntax-highlighter-15.6.1.tgz", + "integrity": "sha512-OqJ2/vL7lEeV5zTJyG7kmARppUjiB9h9udl4qHQjjgEos66z00Ia0OckwYfRxCSFrW8RJIBnsBwQsHZbVPspqg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.3.1", + "highlight.js": "^10.4.1", + "highlightjs-vue": "^1.0.0", + "lowlight": "^1.17.0", + "prismjs": "^1.27.0", + "refractor": "^3.6.0" + }, + "peerDependencies": { + "react": ">= 0.14.0" + } + }, "node_modules/react-use-measure": { "version": "2.1.7", "resolved": "https://registry.npmjs.org/react-use-measure/-/react-use-measure-2.1.7.tgz", @@ -10395,6 +10557,122 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/refractor": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/refractor/-/refractor-3.6.0.tgz", + "integrity": "sha512-MY9W41IOWxxk31o+YvFCNyNzdkc9M20NoZK5vq6jkv4I/uh2zkWcfudj0Q1fovjUQJrNewS9NMzeTtqPf+n5EA==", + "license": "MIT", + "dependencies": { + "hastscript": "^6.0.0", + "parse-entities": "^2.0.0", + "prismjs": "~1.27.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/refractor/node_modules/character-entities": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-1.2.4.tgz", + "integrity": "sha512-iBMyeEHxfVnIakwOuDXpVkc54HijNgCyQB2w0VfGQThle6NXn50zU6V/u+LDhxHcDUPojn6Kpga3PTAD8W1bQw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/refractor/node_modules/character-entities-legacy": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-1.1.4.tgz", + "integrity": "sha512-3Xnr+7ZFS1uxeiUDvV02wQ+QDbc55o97tIV5zHScSPJpcLm/r0DFPcoY3tYRp+VZukxuMeKgXYmsXQHO05zQeA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/refractor/node_modules/character-reference-invalid": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/character-reference-invalid/-/character-reference-invalid-1.1.4.tgz", + "integrity": "sha512-mKKUkUbhPpQlCOfIuZkvSEgktjPFIsZKRRbC6KWVEMvlzblj3i3asQv5ODsrwt0N3pHAEvjP8KTQPHkp0+6jOg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/refractor/node_modules/is-alphabetical": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-1.0.4.tgz", + "integrity": "sha512-DwzsA04LQ10FHTZuL0/grVDk4rFoVH1pjAToYwBrHSxcrBIGQuXrQMtD5U1b0U2XVgKZCTLLP8u2Qxqhy3l2Vg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/refractor/node_modules/is-alphanumerical": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-1.0.4.tgz", + "integrity": "sha512-UzoZUr+XfVz3t3v4KyGEniVL9BDRoQtY7tOyrRybkVNjDFWyo1yhXNGrrBTQxp3ib9BLAWs7k2YKBQsFRkZG9A==", + "license": "MIT", + "dependencies": { + "is-alphabetical": "^1.0.0", + "is-decimal": "^1.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/refractor/node_modules/is-decimal": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-1.0.4.tgz", + "integrity": "sha512-RGdriMmQQvZ2aqaQq3awNA6dCGtKpiDFcOzrTWrDAT2MiWrKQVPmxLGHl7Y2nNu6led0kEyoX0enY0qXYsv9zw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/refractor/node_modules/is-hexadecimal": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-1.0.4.tgz", + "integrity": "sha512-gyPJuv83bHMpocVYoqof5VDiZveEoGoFL8m3BXNb2VW8Xs+rz9kqO8LOQ5DH6EsuvilT1ApazU0pyl+ytbPtlw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/refractor/node_modules/parse-entities": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-2.0.0.tgz", + "integrity": "sha512-kkywGpCcRYhqQIchaWqZ875wzpS/bMKhz5HnN3p7wveJTkTtyAB/AlnS0f8DFSqYW1T82t6yEAkEcB+A1I3MbQ==", + "license": "MIT", + "dependencies": { + "character-entities": "^1.0.0", + "character-entities-legacy": "^1.0.0", + "character-reference-invalid": "^1.0.0", + "is-alphanumerical": "^1.0.0", + "is-decimal": "^1.0.0", + "is-hexadecimal": "^1.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/refractor/node_modules/prismjs": { + "version": "1.27.0", + "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.27.0.tgz", + "integrity": "sha512-t13BGPUlFDR7wRB5kQDG4jjl7XeuH6jbJGt11JHPL96qwsEHNX2+68tFXqc1/k+/jALsbSWJKUOT/hcYAZ5LkA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/regenerate": { "version": "1.4.2", "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz", @@ -12287,6 +12565,15 @@ "node": ">=8" } }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "license": "MIT", + "engines": { + "node": ">=0.4" + } + }, "node_modules/y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", diff --git a/frontend/package.json b/frontend/package.json index 7d33fdaf..d8e370aa 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -19,6 +19,7 @@ "@rematch/loading": "^2.1.2", "@stripe/react-stripe-js": "^3.5.1", "@stripe/stripe-js": "^6.1.0", + "@types/react-syntax-highlighter": "^15.5.13", "@types/three": "^0.174.0", "clsx": "^2.1.1", "immer": "^10.1.1", @@ -32,6 +33,7 @@ "react-map-gl": "^8.0.1", "react-markdown": "^10.1.0", "react-redux": "^9.2.0", + "react-syntax-highlighter": "^15.6.1", "redux": "^5.0.1", "three": "^0.174.0" }, diff --git a/frontend/public/images/chris.jpg b/frontend/public/images/chris.jpg new file mode 100644 index 0000000000000000000000000000000000000000..4d15821a9eeb98ce0495060b030787a4b69870a6 GIT binary patch literal 18056 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 -
-

{article.title}

-
- Published{' '} - {article.date.toLocaleDateString('en-US', { - weekday: 'long', - year: 'numeric', - month: 'long', - day: 'numeric', - })} +
+
+ + More Articles + + +
+
+ {`By +
+

{article.title}

+
+ Published{' '} + {article.date.toLocaleDateString('en-US', { + weekday: 'long', + year: 'numeric', + month: 'long', + day: 'numeric', + })} +
+
{article.content}
diff --git a/frontend/src/app/(public-area)/articles/capital-one-data-breach.tsx b/frontend/src/app/(public-area)/articles/capital-one-data-breach.tsx index db93bd06..04b4b314 100644 --- a/frontend/src/app/(public-area)/articles/capital-one-data-breach.tsx +++ b/frontend/src/app/(public-area)/articles/capital-one-data-breach.tsx @@ -1,5 +1,9 @@ const article = { title: 'Capital One Data Breach – A Cautionary Tale', + author: { + name: 'Chris', + image: '/images/chris.jpg', + }, description: 'Capital One lost hundreds of millions after being notified by a third party of an intruder that had been lurking in their AWS account for four months.', date: new Date(Date.parse('2025-04-15T23:10:00-04:00')), diff --git a/frontend/src/app/(public-area)/articles/how-to-allow-aws-principals-to-modify-only-resources-they-create.tsx b/frontend/src/app/(public-area)/articles/how-to-allow-aws-principals-to-modify-only-resources-they-create.tsx new file mode 100644 index 00000000..771dd032 --- /dev/null +++ b/frontend/src/app/(public-area)/articles/how-to-allow-aws-principals-to-modify-only-resources-they-create.tsx @@ -0,0 +1,235 @@ +import { SyntaxHighlighter } from '@/components/SyntaxHighlighter'; +import Link from 'next/link'; + +const arnBasedExample = `{ + "Effect": "Allow", + "Action": [ + "ecs:CreateCluster", + "ecs:DeleteCluster" + ], + "Resource": [ + "arn:aws:ec2:us-east-1:123412341234:cluster/MyCoolService-*" + ] +}`; + +const ec2Example = `[{ + "Effect": "Allow", + "Action": "ec2:RunInstances", + "Resource": "*" +}, { + "Effect": "Allow", + "Action": "ec2:CreateTags", + "Resource": "arn:aws:ec2:us-east-1:123412341234:instance/*", + "Condition": { + "StringEquals": { + "ec2:CreateAction" : "RunInstances" + } + } +}, { + "Effect": "Allow", + "Action": "ec2:StopInstances", + "Resource": "arn:aws:ec2:us-east-1:123412341234:instance/*", + "Condition": { + "StringEquals": { + "aws:ResourceTag/ManagedByMyCoolService" : "true" + } + } +}]`; + +const hiddenTechniqueExample = `{ + "Effect": "Allow", + "Action": [ + "ec2:RunInstances", + "ec2:StopInstances", + "ec2:CreateTags" + ], + "Resource": "arn:aws:ec2:us-east-1:123412341234:instance/*", + "Condition": { + "StringEquals": { + "aws:ResourceTag/ManagedByMyCoolService" : "true" + } + } +}`; + +const hiddenTechniqueCombinedExample = `{ + "Effect": "Allow", + "Action": [ + "ec2:RunInstances", + "ec2:StopInstances", + "ec2:CreateTags", + "ecs:CreateCluster", + "ecs:DeleteCluster", + "ecs:TagResource" + ], + "Resource": "*", + "Condition": { + "StringEquals": { + "aws:ResourceTag/ManagedByMyCoolService" : "true" + } + } +}`; + +const tryIt = [ + '# Create two clusters, one owned by our cool service and one that is not.', + 'aws --profile admin ecs create-cluster --cluster-name uncool-service-cluster', + `aws --profile my-cool-service ecs create-cluster --cluster-name my-cool-service-cluster --tags 'key=ManagedByMyCoolService,value=true'`, + '', + "# Try to delete the uncool service cluster (It won't work)", + 'aws --profile my-cool-service ecs delete-cluster --cluster uncool-service-cluster', + 'An error occurred (AccessDeniedException) when calling the DeleteCluster operation: User: arn:aws:iam::********:user/my-cool-service is not authorized to perform: ecs:DeleteCluster on resource: arn:aws:ecs:us-east-1:********:cluster/uncool-service-cluster because no identity-based policy allows the ecs:DeleteCluster action', + '', + '# But we can delete our own cluster', + 'aws --profile my-cool-service ecs delete-cluster --cluster my-cool-service-cluster', + '', + '# And we cannot add our tag to the uncool service cluster', + `aws --profile my-cool-service ecs tag-resource --resource-arn arn:aws:ecs:us-east-1:********:cluster/uncool-service-cluster --tags 'key=ManagedByMyCoolService,value=true'`, + 'An error occurred (AccessDeniedException) when calling the TagResource operation: User: arn:aws:iam::********:user/my-cool-service is not authorized to perform: ecs:TagResource on resource: arn:aws:ecs:us-east-1:********:cluster/uncool-service-cluster because no identity-based policy allows the ecs:TagResource action', +]; + +const article = { + title: 'How To Allow AWS Principals To Modify Only Resources They Create', + author: { + name: 'Chris', + image: '/images/chris.jpg', + }, + description: + 'Use this hidden technique if you want to allow users or services to create and modify resources without giving them access to any pre-existing resources.', + date: new Date(Date.parse('2025-04-22T12:05:00-04:00')), + content: ( + <> +

Why would you want to do that anyway?

+

+ If you're serious about enforcing least privilege, you've probably run into a situation like + this before: +

+

+ You've written an amazing cloud-native service that you want to deploy to AWS, so you begin working + on your infrastructure-as-code and get to the point where you need to write the IAM policy for your + service. Your service is going to be deployed to an account that has other services running in it, so + you and your InfoSec people want to make absolutely sure that it can't access or interfere with + resources that don't belong to it. +

+

+ In particular, your service needs to be able to create and delete ECS clusters, but you want to make + sure that it can't delete clusters that don't belong to it. So you decide on a prefix for your + cluster names and grant your service permissions like so: +

+ {arnBasedExample} +

Mission accomplished! Everyone is happy.

+

Trouble in Paradise

+

+ You continue writing out your IAM policy and realize that your service also needs to be able to start + and stop EC2 instances. Again, you don't want your service to be able to stop any EC2 instances + that don't belong to it. +

+

+ Unfortunately, you have no control over the ARN for EC2 instances, so you can't use the same trick. + But after some digging, you come up with a clever solution that combines{' '} + + attributed-based access control + {' '} + (ABAC) and{' '} + + ec2:CreateAction + + : +

+ {ec2Example} +

+ You've done it again! Your service can only stop instances with your special tag, which it can add + to new instances, but not to pre-existing instances. +

+

The Hidden Technique 🥷

+

+ Cloud Snitch recently gained the ability to restrict account activity via{' '} + + service control policies + + . When implementing this functionality, we wanted to be absolutely sure that when you grant the required + permissions to Cloud Snitch, you can rest assured knowing that it is literally impossible for Cloud + Snitch to do anything that would reduce your security posture. That means Cloud Snitch should be able to + create service control policies, but not modify, detach, or delete policies that it didn't create + for you. +

+

+ Unfortunately, there's no equivalent to `ec2:CreateAction` for service control policies. In fact, + the vast majority of resources in AWS don't have an equivalent condition that can be used. +

+

+ However, if we dig deep, we can unlock a secret jutsu that works for any AWS resource that supports + tagging. The key is in{' '} + + the documentation for aws:ResourceTag + + : +

+
+ This key is included in the request context when the requested resource already has attached tags or in + requests that create a resource with an attached tag. This key is returned only for resources that + support authorization based on tags. There is one context key for each tag key-value pair. +
+

+ Note that the when the resource already exists, aws:ResourceTag{' '} + refers to the tags already on the resource. However, when the resource is being created,{' '} + aws:ResourceTag refers to the desired tags for the + resource. +

+

This means the above example could also be written like this:

+ {hiddenTechniqueExample} +

+ This allows your service to manipulate its own instances, while enforcing least privilege, and without + relying on service-specific conditions. It also has the benefit of being concise! +

+

+ You could even combine multiple services together into a single statement without sacrificing security: +

+ {hiddenTechniqueCombinedExample} +

Try it Yourself

+

+ Don't take our word for it. Try it out yourself. Configure your AWS CLI with two sets of + credentials: one with admin powers and one with the above statement, then... +

+ {tryIt.join('\n')} + + ), + relatedLinks: [ + { + title: 'Control access using attribute-based access', + url: 'https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/iam-policies-for-amazon-ec2.html#control-access-with-tags', + }, + { + title: 'Grant permission to tag Amazon EC2 resources during creation', + url: 'https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/supported-iam-actions-tagging.html', + }, + { + title: 'Controlling access to AWS resources using tags', + url: 'https://docs.aws.amazon.com/IAM/latest/UserGuide/access_tags.html', + }, + { + title: 'Service control policies (SCPs)', + url: 'https://docs.aws.amazon.com/organizations/latest/userguide/orgs_manage_policies_scps.html', + }, + ], +}; + +export default article; diff --git a/frontend/src/app/(public-area)/articles/index.tsx b/frontend/src/app/(public-area)/articles/index.tsx index 8f17216b..01943d36 100644 --- a/frontend/src/app/(public-area)/articles/index.tsx +++ b/frontend/src/app/(public-area)/articles/index.tsx @@ -1,7 +1,12 @@ import CapitalOneDataBreach from './capital-one-data-breach'; +import AwsModifyOnlyCreatedResource from './how-to-allow-aws-principals-to-modify-only-resources-they-create'; interface Article { title: string; + author: { + name: string; + image: string; + }; description: string; date: Date; content: React.ReactNode; @@ -10,4 +15,5 @@ interface Article { export const articles: Record = { 'capital-one-data-breach': CapitalOneDataBreach, + 'how-to-allow-aws-principals-to-modify-only-resources-they-create': AwsModifyOnlyCreatedResource, }; diff --git a/frontend/src/app/(public-area)/articles/page.tsx b/frontend/src/app/(public-area)/articles/page.tsx index e906dbac..0170dd3a 100644 --- a/frontend/src/app/(public-area)/articles/page.tsx +++ b/frontend/src/app/(public-area)/articles/page.tsx @@ -1,5 +1,6 @@ import { ChevronRightIcon } from '@heroicons/react/24/outline'; import type { Metadata } from 'next'; +import Image from 'next/image'; import Link from 'next/link'; import { articles } from '.'; @@ -19,14 +20,25 @@ const Page = () => {
{sortedArticles.map(([slug, article]) => (
-

{article.title}

-
- {article.date.toLocaleDateString('en-US', { - weekday: 'long', - year: 'numeric', - month: 'long', - day: 'numeric', - })} +
+ {`By +
+

{article.title}

+
+ {article.date.toLocaleDateString('en-US', { + weekday: 'long', + year: 'numeric', + month: 'long', + day: 'numeric', + })} +
+

{article.description}

diff --git a/frontend/src/app/(public-area)/page.tsx b/frontend/src/app/(public-area)/page.tsx index 6784ad93..4ecb2c4f 100644 --- a/frontend/src/app/(public-area)/page.tsx +++ b/frontend/src/app/(public-area)/page.tsx @@ -60,8 +60,8 @@ const Page = () => {

Check out our features to learn how we do it.

-
- +
+ Learn More @@ -82,14 +82,17 @@ const Page = () => { had been lurking in their AWS account for four months.

-
+
Read More + + More Articles +
diff --git a/frontend/src/app/(user-area)/teams/[teamId]/Rules.tsx b/frontend/src/app/(user-area)/teams/[teamId]/Rules.tsx index 9b748c4e..790632b8 100644 --- a/frontend/src/app/(user-area)/teams/[teamId]/Rules.tsx +++ b/frontend/src/app/(user-area)/teams/[teamId]/Rules.tsx @@ -5,7 +5,7 @@ import Link from 'next/link'; import { Transition } from '@headlessui/react'; import { awsServices } from '@/aws'; -import { Button, ChipEditor, Dialog, ErrorMessage, SuccessMessage } from '@/components'; +import { Button, ChipEditor, Dialog, ErrorMessage, SuccessMessage, SyntaxHighlighter } from '@/components'; import { AWSAccount } from '@/generated/api'; import { useAwsRegions, useCurrentTeamId, useManagedAwsScp, useTeamAwsAccountsMap } from '@/hooks'; import { RuleSet } from '@/rules'; @@ -56,9 +56,12 @@ const PolicyPreview = ({ account, onSuccess, ruleSet }: PolicyPreviewProps) => { Please exercise caution as it is possible to lock yourself out or disrupt services running in the account.

-
-                {prettyContent}
-            
+ + {prettyContent} +
); diff --git a/frontend/src/app/globals.css b/frontend/src/app/globals.css index 5e2c3b9c..62b3d0b7 100644 --- a/frontend/src/app/globals.css +++ b/frontend/src/app/globals.css @@ -201,4 +201,9 @@ body { padding: 0 calc(var(--spacing) * 1); font-size: var(--text-sm); } + + .quote { + padding-left: calc(var(--spacing) * 1); + border-left: 2px solid var(--color-platinum); + } } diff --git a/frontend/src/components/Markdown.tsx b/frontend/src/components/Markdown.tsx index a35f7258..5e04b81f 100644 --- a/frontend/src/components/Markdown.tsx +++ b/frontend/src/components/Markdown.tsx @@ -29,7 +29,7 @@ export const Markdown = ({ children }: Props) => { }, blockquote(props) { const { children } = props; - return
{children}
; + return
{children}
; }, code(props) { const { children, node } = props; diff --git a/frontend/src/components/SyntaxHighlighter.tsx b/frontend/src/components/SyntaxHighlighter.tsx new file mode 100644 index 00000000..7ea925ba --- /dev/null +++ b/frontend/src/components/SyntaxHighlighter.tsx @@ -0,0 +1,26 @@ +import { Light as SyntaxHighlighterImpl } from 'react-syntax-highlighter'; +import bash from 'react-syntax-highlighter/dist/esm/languages/hljs/bash'; +import json from 'react-syntax-highlighter/dist/esm/languages/hljs/json'; +import style from 'react-syntax-highlighter/dist/esm/styles/hljs/github'; + +SyntaxHighlighterImpl.registerLanguage('bash', bash); +SyntaxHighlighterImpl.registerLanguage('json', json); + +interface Props { + language: 'bash' | 'json'; + children: string | string[]; + className?: string; +} + +export const SyntaxHighlighter = ({ className, language, children }: Props) => { + return ( + + {children} + + ); +}; diff --git a/frontend/src/components/index.tsx b/frontend/src/components/index.tsx index 6546ba65..374dbb88 100644 --- a/frontend/src/components/index.tsx +++ b/frontend/src/components/index.tsx @@ -15,6 +15,7 @@ export { IndividualSubscriptionBox, TeamSubscriptionBox } from './SubscriptionBo export { Select } from './Select'; export { SubscriptionSelector } from './SubscriptionSelector'; export { SuccessMessage } from './SuccessMessage'; +export { SyntaxHighlighter } from './SyntaxHighlighter'; export { TabLayout } from './TabLayout'; export { TextArea } from './TextArea'; export { TextField } from './TextField';