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 00000000..4d15821a Binary files /dev/null and b/frontend/public/images/chris.jpg differ diff --git a/frontend/src/app/(public-area)/articles/[slug]/page.tsx b/frontend/src/app/(public-area)/articles/[slug]/page.tsx index 47ac85d9..492c1479 100644 --- a/frontend/src/app/(public-area)/articles/[slug]/page.tsx +++ b/frontend/src/app/(public-area)/articles/[slug]/page.tsx @@ -1,4 +1,6 @@ +import { ArrowUturnLeftIcon } from '@heroicons/react/24/outline'; import type { Metadata } from 'next'; +import Image from 'next/image'; import Link from 'next/link'; import { articles } from '..'; @@ -35,16 +37,33 @@ export default async function Page({ params }: Props) { return (
-
-

{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';